Path:
strictdoc/core/project_config.py
Lines:
1388
Non-empty lines:
1196
Non-empty lines covered with requirements:
1196 / 1196 (100.0%)
Functions:
53
Functions covered by requirements:
53 / 53 (100.0%)
- "13.2. Feature toggles" (REQUIREMENT)
- "13.1. strictdoc.toml file" (REQUIREMENT)
1
"""2
@relation(SDOC-SRS-39, scope=file)3
"""4
5
import datetime
6
import mimetypes
7
import os
8
import re
9
import tempfile
10
import types
11
from dataclasses import dataclass, field
12
from enum import Enum
13
from pathlib import Path
14
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
15
16
import toml
17
18
from strictdoc import __version__, environment
19
from strictdoc.backend.reqif.sdoc_reqif_fields import ReqIFProfile
20
from strictdoc.backend.sdoc.constants import SDocMarkup
21
from strictdoc.commands.convert_config import ConvertCommandConfig
22
from strictdoc.commands.export_config import ExportCommandConfig
23
from strictdoc.commands.format_config import FormatCommandConfig
24
from strictdoc.commands.manage_autouid_config import ManageAutoUIDCommandConfig
25
from strictdoc.commands.manage_new_config import ManageNewCommandConfig
26
from strictdoc.commands.server_config import ServerCommandConfig
27
from strictdoc.core.environment import SDocRuntimeEnvironment
28
from strictdoc.core.feature import Feature
29
from strictdoc.core.plugin import StrictDocPlugin
30
from strictdoc.helpers.auto_described import auto_described
31
from strictdoc.helpers.deprecation_engine import DEPRECATION_ENGINE
32
from strictdoc.helpers.exception import StrictDocException
33
from strictdoc.helpers.file_modification_time import get_file_modification_time
34
from strictdoc.helpers.md5 import get_md5
35
from strictdoc.helpers.module import import_from_path
36
from strictdoc.helpers.net import is_valid_host
37
from strictdoc.helpers.path_filter import validate_mask
38
39
if TYPE_CHECKING:
40
from strictdoc.core.format import Format
41
42
43
def parse_relation_tuple(column_name: str) -> Optional[Tuple[str, str]]:
44
match_result = re.search(
45
r"^((Parent|Child|File)?)(\[(.{1,32})])?$", column_name
46
)47
if match_result is None:
48
return None
49
return match_result.group(1), match_result.group(4)
50
51
52
@dataclass53
class SourceNodesEntry:
54
path: str
55
uid: str
56
node_type: str
57
sdoc_to_source_map: Dict[str, str] = field(default_factory=dict)
58
full_path: Optional[Path] = None
59
60
61
class ProjectFeature(str, Enum):
62
# Stable features.63
TABLE_SCREEN = "TABLE_SCREEN"
64
TRACEABILITY_SCREEN = "TRACEABILITY_SCREEN"
65
DEEP_TRACEABILITY_SCREEN = "DEEP_TRACEABILITY_SCREEN"
66
67
MATHJAX = "MATHJAX"
68
69
# Experimental features.70
SEARCH = "SEARCH"
71
HTML2PDF = "HTML2PDF"
72
REQIF = "REQIF"
73
DIFF = "DIFF"
74
PROJECT_STATISTICS_SCREEN = "PROJECT_STATISTICS_SCREEN"
75
TREE_MAP_SCREEN = "TREE_MAP_SCREEN"
76
TRACEABILITY_MATRIX_SCREEN = "TRACEABILITY_MATRIX_SCREEN"
77
REQUIREMENT_TO_SOURCE_TRACEABILITY = "REQUIREMENT_TO_SOURCE_TRACEABILITY"
78
SOURCE_FILE_LANGUAGE_PARSERS = "SOURCE_FILE_LANGUAGE_PARSERS"
79
80
MERMAID = "MERMAID"
81
NESTOR = "NESTOR"
82
83
ALL_FEATURES = "ALL_FEATURES"
84
85
@staticmethod86
def all() -> List[str]: # noqa: A003
87
return list(map(lambda c: c.value, ProjectFeature))
88
89
90
class ProjectConfigDefault:
91
DEFAULT_PROJECT_TITLE = "Untitled Project"
92
DEFAULT_DIR_FOR_SDOC_ASSETS = "_static"
93
DEFAULT_DIR_FOR_OUTPUT = "output"
94
DEFAULT_DIR_FOR_SDOC_CACHE = "output/_cache"
95
96
DEFAULT_FEATURES: List[str] = [
97
ProjectFeature.TABLE_SCREEN,
98
ProjectFeature.TRACEABILITY_SCREEN,
99
ProjectFeature.DEEP_TRACEABILITY_SCREEN,
100
ProjectFeature.SEARCH,
101
]102
DEFAULT_SERVER_HOST = "127.0.0.1"
103
DEFAULT_SERVER_PORT = 5111
104
DEFAULT_BUNDLE_DOCUMENT_VERSION = "@GIT_VERSION (Git branch: @GIT_BRANCH)"
105
DEFAULT_BUNDLE_DOCUMENT_COMMIT_DATE = "@GIT_COMMIT_DATETIME"
106
DEFAULT_SECTION_BEHAVIOR = "[SECTION]"
107
108
109
def resolve_favicon_variant(
110
environment: SDocRuntimeEnvironment, is_running_on_server: bool
111
) -> str:
112
"""Resolve which favicon.svg.jinja variant identifies this process."""
113
if environment.is_test_env:
114
return "test"
115
if environment.is_development_mode:
116
return "dev"
117
if not is_running_on_server:
118
return "export"
119
return "default"
120
121
122
@auto_described123
class ProjectConfig:
124
"""
125
@relation(SDOC-SRS-119, scope=class)126
"""127
128
def __init__(
129
self,
130
*,
131
project_title: str = ProjectConfigDefault.DEFAULT_PROJECT_TITLE,
132
dir_for_sdoc_assets: str = ProjectConfigDefault.DEFAULT_DIR_FOR_SDOC_ASSETS,
133
dir_for_sdoc_cache: str = ProjectConfigDefault.DEFAULT_DIR_FOR_SDOC_CACHE,
134
project_features: Optional[List[Union[str, Feature]]] = None,
135
server_host: str = ProjectConfigDefault.DEFAULT_SERVER_HOST,
136
server_port: int = ProjectConfigDefault.DEFAULT_SERVER_PORT,
137
input_paths: Optional[List[str]] = None,
138
include_doc_paths: Optional[List[str]] = None,
139
exclude_doc_paths: Optional[List[str]] = None,
140
source_root_path: Optional[str] = None,
141
include_source_paths: Optional[List[str]] = None,
142
exclude_source_paths: Optional[List[str]] = None,
143
grammars: Optional[Dict[str, str]] = None,
144
test_report_root_dict: Optional[Dict[str, str]] = None,
145
source_nodes: Optional[List[SourceNodesEntry]] = None,
146
html2pdf_strict: bool = False,
147
html2pdf_template: Optional[str] = None,
148
html2pdf_forced_page_break_nodes: Optional[List[str]] = None,
149
bundle_document_uid: Optional[str] = None,
150
bundle_document_version: Optional[
151
str152
] = ProjectConfigDefault.DEFAULT_BUNDLE_DOCUMENT_VERSION,
153
bundle_document_date: Optional[
154
str155
] = ProjectConfigDefault.DEFAULT_BUNDLE_DOCUMENT_COMMIT_DATE,
156
traceability_matrix_relation_columns: Optional[
157
List[Tuple[str, Optional[str]]]
158
] = None,
159
reqif_profile: str = ReqIFProfile.P01_SDOC,
160
# FIXME: Change to true by default.161
reqif_multiline_is_xhtml: bool = False,
162
# FIXME: Change to true by default.163
reqif_enable_mid: bool = False,
164
reqif_import_markup: Optional[str] = None,
165
diff_git_revisions: Optional[str] = None,
166
diff_dir_revisions: Optional[Tuple[str, str]] = None,
167
chromedriver: Optional[str] = None,
168
# FIXME: The section_behavior field will be removed by the end of 2025-Q4.169
section_behavior: Optional[
170
str171
] = ProjectConfigDefault.DEFAULT_SECTION_BEHAVIOR,
172
statistics_generator: Optional[str] = None,
173
document_line_width: Optional[int] = None,
174
# Logo path can be set in the project config to customize the launcher's appearance for a specific project.175
launcher_logo_path: Optional[str] = None,
176
# Favicon path can be set in the project config to customize the177
# browser-tab favicon for a project's own (non-dev, non-test) server178
# or static export. Ignored for the dev/test favicon variants.179
favicon_path: Optional[str] = None,
180
# Custom CSS path can be set in the project config to extend or181
# override StrictDoc's default stylesheets in the HTML export and182
# on the server.183
custom_css_path: Optional[str] = None,
184
user_plugin: Optional[StrictDocPlugin] = None,
185
formats: Optional[List["Format"]] = None,
186
# Reserved for StrictDoc's internal use.187
_config_last_update: Optional[datetime.datetime] = None,
188
) -> None:
189
self.environment: SDocRuntimeEnvironment = environment
190
191
# Settings obtained from the strictdoc.toml config file.192
self.project_title: str = project_title
193
self.dir_for_sdoc_assets: str = dir_for_sdoc_assets
194
195
if env_cache_dir := os.environ.get("STRICTDOC_CACHE_DIR"):
196
# The only use case for STRICTDOC_CACHE_DIR is to make the cache197
# local to an itest folder.198
assert env_cache_dir == "Output/_cache", env_cache_dir
199
dir_for_sdoc_cache = env_cache_dir
200
elif dir_for_sdoc_cache == "$TMPDIR":
201
dir_for_sdoc_cache = os.path.join(
202
tempfile.gettempdir(),
203
"strictdoc_cache",
204
get_md5(os.getcwd()),
205
)206
207
# Adding a __version__ part to the cache directory improves traceability208
# by indicating which StrictDoc version the cache belongs to.209
# This helps prevent issues when switching between versions that may use210
# incompatible cache schemas.211
dir_for_sdoc_cache = os.path.join(dir_for_sdoc_cache, __version__)
212
213
self.dir_for_sdoc_cache: str = dir_for_sdoc_cache
214
215
#216
# project_features217
#218
project_features_: List[Union[str, Feature]] = (
219
project_features220
if project_features is not None
221
else list(ProjectConfigDefault.DEFAULT_FEATURES)
222
)223
224
assert isinstance(project_features_, list), (
225
f"config: project_features: parameter must be an "
226
f"array: '{project_features_}'."
227
)228
229
for feature in project_features_:
230
if isinstance(feature, Feature):
231
continue232
assert feature in ProjectFeature.all(), (
233
f"config: project_features: unknown feature declared: "
234
f"'{feature}'."
235
)236
237
if ProjectFeature.ALL_FEATURES in project_features_:
238
custom_features = [
239
feature240
for feature in project_features_
241
if isinstance(feature, Feature)
242
]243
project_features_ = [*ProjectFeature.all(), *custom_features]
244
245
self.project_features: List[Union[str, Feature]] = project_features_
246
247
#248
# server_host and server_port249
#250
assert is_valid_host(server_host), (
251
f"config: server_host: invalid host: {server_host}'."
252
)253
self.server_host: str = server_host
254
255
assert isinstance(server_port, int) and 1024 < server_port < 65000, (
256
f"strictdoc.toml: 'port': invalid port: {server_port}'."
257
)258
self.server_port: int = server_port
259
260
#261
# input_paths262
#263
self.input_paths: Optional[List[str]] = input_paths
264
265
#266
# include_doc_paths267
#268
include_doc_paths = include_doc_paths or []
269
assert isinstance(include_doc_paths, list), include_doc_paths
270
for include_doc_path in include_doc_paths:
271
try:
272
validate_mask(include_doc_path)
273
except SyntaxError as exception_:
274
raise ValueError(
275
f"config: include_doc_paths: {exception_}"
276
) from exception_
277
self.include_doc_paths: List[str] = include_doc_paths
278
279
#280
# exclude_doc_paths281
#282
exclude_doc_paths = exclude_doc_paths or []
283
assert isinstance(exclude_doc_paths, list), exclude_doc_paths
284
for exclude_doc_path in exclude_doc_paths:
285
try:
286
validate_mask(exclude_doc_path)
287
except SyntaxError as exception_:
288
raise ValueError(
289
f"config: exclude_doc_paths: {exception_}"
290
) from exception_
291
self.exclude_doc_paths: List[str] = exclude_doc_paths
292
293
#294
# include_source_paths295
#296
include_source_paths = include_source_paths or []
297
assert isinstance(include_source_paths, list), include_source_paths
298
for include_source_path in include_source_paths:
299
try:
300
validate_mask(include_source_path)
301
except SyntaxError as exception_:
302
raise ValueError(
303
f"config: include_source_paths: {exception_}"
304
) from exception_
305
self.include_source_paths: List[str] = include_source_paths
306
307
#308
# exclude_source_paths309
#310
exclude_source_paths = exclude_source_paths or []
311
assert isinstance(exclude_source_paths, list), exclude_source_paths
312
for exclude_source_path in exclude_source_paths:
313
try:
314
validate_mask(exclude_source_path)
315
except SyntaxError as exception_:
316
raise ValueError(
317
f"config: exclude_source_paths: {exception_}"
318
) from exception_
319
self.exclude_source_paths: List[str] = exclude_source_paths
320
321
#322
# source_root_path323
#324
self.source_root_path: Optional[str] = source_root_path
325
326
#327
# grammars - Grammar aliases.328
#329
self.grammars: Dict[str, str] = grammars or {}
330
331
self.test_report_root_dict: Dict[str, str] = (
332
test_report_root_dict if test_report_root_dict is not None else {}
333
)334
self.source_nodes: List[SourceNodesEntry] = (
335
source_nodes if source_nodes is not None else []
336
)337
338
# Settings derived from the command-line parameters.339
340
# Common settings.341
self.output_dir: str = ProjectConfigDefault.DEFAULT_DIR_FOR_OUTPUT
342
343
# Export action.344
self.export_output_html_root: str = os.path.join(
345
self.output_dir, "html"
346
)347
self.export_formats: Optional[List[str]] = None
348
self.formats: List[Format] = (
349
formats if formats is not None else ProjectConfig.default_formats()
350
)351
self.export_included_documents: bool = False
352
self.generate_bundle_document: bool = False
353
self.filter_nodes: Optional[str] = None
354
355
self.excel_export_fields: Optional[List[str]] = None
356
357
assert isinstance(html2pdf_strict, bool), (
358
"config: html2pdf_strict: "359
f"must be a True/False value: {html2pdf_strict}."
360
)361
self.html2pdf_strict: bool = html2pdf_strict
362
363
self.html2pdf_template: Optional[str] = html2pdf_template
364
365
if html2pdf_forced_page_break_nodes is not None:
366
assert isinstance(html2pdf_forced_page_break_nodes, list)
367
assert len(html2pdf_forced_page_break_nodes) <= 10
368
self.html2pdf_forced_page_break_nodes: List[str] = (
369
html2pdf_forced_page_break_nodes or []
370
)371
372
self.bundle_document_uid: Optional[str] = bundle_document_uid
373
self.bundle_document_version: Optional[str] = bundle_document_version
374
self.bundle_document_date: Optional[str] = bundle_document_date
375
376
self.traceability_matrix_relation_columns: Optional[
377
List[Tuple[str, Optional[str]]]
378
] = traceability_matrix_relation_columns
379
380
#381
# ReqIF382
#383
self.reqif_profile: str = reqif_profile
384
385
assert isinstance(reqif_multiline_is_xhtml, bool), (
386
reqif_multiline_is_xhtml387
)388
self.reqif_multiline_is_xhtml: bool = reqif_multiline_is_xhtml
389
390
assert isinstance(reqif_enable_mid, bool), reqif_enable_mid
391
self.reqif_enable_mid: bool = reqif_enable_mid
392
393
if reqif_import_markup is not None:
394
assert reqif_import_markup in SDocMarkup.ALL, (
395
"config: reqif_import_markup: expected a valid markup: "396
f"({SDocMarkup.ALL}). Got: "
397
f"'{reqif_import_markup}'."
398
)399
400
self.reqif_import_markup: Optional[str] = reqif_import_markup
401
402
#403
# auto_uid_mode: default is False. The True-case is used by the404
# manage/auto_uid command: the SDocNodeValidator will405
# not raise an exception if it sees a node with a missing UID.406
# Important for a special case:407
# The Manage UID command auto-generates the UID, so the field presence408
# validation has to be relaxed.409
# The GitHub issue report:410
# manage auto-uid: UID field REQUIRED True leads to an error411
# https://github.com/strictdoc-project/strictdoc/issues/1896412
#413
self.auto_uid_mode = False
414
self.autouuid_include_sections: bool = False
415
416
self.view: Optional[str] = None
417
418
self.diff_git_revisions: Optional[str] = diff_git_revisions
419
self.diff_dir_revisions: Optional[Tuple[str, str]] = diff_dir_revisions
420
421
self.chromedriver: Optional[str] = chromedriver
422
self.section_behavior: Optional[str] = section_behavior
423
424
self.statistics_generator: Optional[str] = statistics_generator
425
426
if document_line_width is not None:
427
assert isinstance(document_line_width, int), document_line_width
428
assert document_line_width >= 80, (
429
f"config: document_line_width: minimum acceptable value is 80, "
430
f"got: {document_line_width}."
431
)432
self.document_line_width: Optional[int] = document_line_width
433
434
self.user_plugin: Optional[StrictDocPlugin] = user_plugin
435
436
# Optional launcher logo path (absolute or workspace-relative).437
self.launcher_logo_path: Optional[str] = launcher_logo_path
438
439
# Optional custom favicon path, project-relative. Validated and440
# resolved to an absolute path in validate_and_finalize().441
self.favicon_path: Optional[str] = favicon_path
442
443
# Optional custom CSS path, project-relative. Validated and444
# resolved to an absolute path in validate_and_finalize().445
self.custom_css_path: Optional[str] = custom_css_path
446
447
self.config_last_update: Optional[datetime.datetime] = (
448
_config_last_update449
)450
self.is_running_on_server: bool = False
451
self.watch_enabled: bool = False
452
453
@staticmethod454
def default_config() -> "ProjectConfig":
455
return ProjectConfig()
456
457
@staticmethod458
def default_formats() -> List["Format"]:
459
# Imported locally to avoid a circular import: each Format module460
# imports generator/writer classes that, transitively, import461
# ProjectConfig itself.462
from strictdoc.backend.excel.export.excel_format import ( # noqa: PLC0415
463
ExcelFormat,
464
)465
from strictdoc.backend.gcov.gcov_format import ( # noqa: PLC0415
466
GCovJSONFormat,
467
)468
from strictdoc.backend.json.json_format import ( # noqa: PLC0415
469
JSONFormat,
470
)471
from strictdoc.backend.markdown.markdown_format import ( # noqa: PLC0415
472
MarkdownFormat,
473
)474
from strictdoc.backend.reqif.reqif_format import ( # noqa: PLC0415
475
ReqIFFormat,
476
)477
from strictdoc.backend.rst.rst_format import ( # noqa: PLC0415
478
RSTFormat,
479
)480
from strictdoc.backend.sdoc.sdoc_format import ( # noqa: PLC0415
481
SDocFormat,
482
)483
from strictdoc.backend.sdoc_source_code.test_reports.junit_xml_format import ( # noqa: PLC0415
484
JUnitXMLFormat,
485
)486
from strictdoc.backend.sdoc_source_code.test_reports.robot_xml_format import ( # noqa: PLC0415
487
RobotXMLFormat,
488
)489
from strictdoc.backend.spdx.spdx_format import ( # noqa: PLC0415
490
SPDXFormat,
491
)492
from strictdoc.export.html.html_format import ( # noqa: PLC0415
493
HTMLFormat,
494
)495
from strictdoc.features.doxygen.doxygen_format import ( # noqa: PLC0415
496
DoxygenFormat,
497
)498
from strictdoc.features.html2pdf.html2pdf_format import ( # noqa: PLC0415
499
HTML2PDFFormat,
500
)501
502
return [
503
HTMLFormat(),
504
HTML2PDFFormat(),
505
RSTFormat(),
506
ExcelFormat(),
507
ReqIFFormat(),
508
SDocFormat(),
509
MarkdownFormat(),
510
DoxygenFormat(),
511
SPDXFormat(),
512
JUnitXMLFormat(),
513
GCovJSONFormat(),
514
RobotXMLFormat(),
515
JSONFormat(),
516
]517
518
@staticmethod519
def _builtin_features_by_handle() -> Dict[str, Feature]:
520
# Imported locally to avoid a circular import, mirroring521
# default_formats() above.522
from strictdoc.features.project_statistics.feature import ( # noqa: PLC0415
523
ProjectStatisticsFeature,
524
)525
526
return {
527
feature.HANDLE: feature for feature in [ProjectStatisticsFeature()]
528
}529
530
def get_features(self) -> List[Feature]:
531
"""
532
Resolve self.project_features (a mix of built-in string handles and533
directly-registered Feature instances) into concrete Feature534
instances. A string handle that isn't backed by a built-in Feature535
yet (i.e. most ProjectFeature enum members, which have not been536
migrated to the Feature abstraction) resolves to nothing here.537
"""538
builtin_features_by_handle = ProjectConfig._builtin_features_by_handle()
539
resolved_features: List[Feature] = []
540
for feature in self.project_features:
541
if isinstance(feature, Feature):
542
resolved_features.append(feature)
543
elif feature in builtin_features_by_handle:
544
resolved_features.append(builtin_features_by_handle[feature])
545
return resolved_features
546
547
def get_feature(self, handle: str) -> Optional[Feature]:
548
for feature in self.get_features():
549
if feature.HANDLE == handle:
550
return feature
551
return None
552
553
def get_editable_document_extensions(self) -> List[str]:
554
"""
555
File extensions a new/edited document may use, i.e. the union of556
supported_extensions() from every format in self.formats that557
supports_edit(). Computed live from self.formats so that adding or558
removing a format changes the accepted extensions without any559
further code changes.560
"""561
extensions: List[str] = []
562
for format_ in self.formats:
563
if not format_.supports_edit():
564
continue565
for extension in format_.supported_extensions():
566
if extension not in extensions:
567
extensions.append(extension)
568
return extensions
569
570
# Some server command settings can override the project config settings.571
def integrate_server_config(
572
self, server_config: ServerCommandConfig
573
) -> None:
574
self.is_running_on_server = True
575
self.watch_enabled = server_config.watch
576
if (server_host_ := server_config.host) is not None:
577
self.server_host = server_host_
578
if (server_port_ := server_config.port) is not None:
579
self.server_port = server_port_
580
581
self.input_paths = [server_config.get_full_input_path()]
582
if self.source_root_path is None:
583
source_root_path = self.input_paths[0]
584
# If the input argument is a relative path, convert it to an585
# absolute path.586
source_root_path = os.path.abspath(source_root_path)
587
source_root_path = source_root_path.rstrip("/")
588
self.source_root_path = source_root_path
589
590
# When setting the output dir, the CLI argument takes precedence.591
output_dir = self.output_dir
592
if server_config.output_path is not None:
593
output_dir = server_config.output_path
594
elif output_dir == ProjectConfigDefault.DEFAULT_DIR_FOR_OUTPUT:
595
output_dir = "./output/server"
596
self.output_dir = output_dir
597
598
self.export_output_html_root = os.path.join(output_dir, "html")
599
600
# If a custom cache folder is not specified in the config, adjust the601
# cache folder to be located in the output folder.602
if self.dir_for_sdoc_cache.startswith(
603
ProjectConfigDefault.DEFAULT_DIR_FOR_SDOC_CACHE
604
):605
self.dir_for_sdoc_cache = os.path.join(
606
output_dir, "_cache", __version__
607
)608
609
self.export_formats = ["html"]
610
self.generate_bundle_document = False
611
self.export_included_documents = True
612
613
def integrate_export_config(
614
self, export_config: ExportCommandConfig
615
) -> None:
616
if export_config.project_title is not None:
617
self.project_title = export_config.project_title
618
619
self.input_paths = export_config.input_paths
620
if self.source_root_path is None:
621
source_root_path = export_config.input_paths[0]
622
# If the input argument is a relative path, convert it to an623
# absolute path.624
source_root_path = os.path.abspath(source_root_path)
625
source_root_path = source_root_path.rstrip("/")
626
self.source_root_path = source_root_path
627
628
#629
# Adjust the default output dir to the user-provided dir if needed.630
#631
output_dir = self.output_dir
632
if export_config.output_dir is not None:
633
output_dir = export_config.output_dir
634
if not os.path.isabs(output_dir):
635
cwd = os.getcwd()
636
output_dir = os.path.join(cwd, output_dir)
637
self.output_dir = output_dir
638
639
# If a custom cache folder is not specified in the config, adjust the640
# cache folder to be located in the output folder.641
if self.dir_for_sdoc_cache.startswith(
642
ProjectConfigDefault.DEFAULT_DIR_FOR_SDOC_CACHE
643
):644
self.dir_for_sdoc_cache = os.path.join(
645
output_dir, "_cache", __version__
646
)647
648
self.export_output_html_root = os.path.join(self.output_dir, "html")
649
self.export_formats = export_config.formats
650
self.export_included_documents = export_config.included_documents
651
self.generate_bundle_document = export_config.generate_bundle_document
652
self.filter_nodes = export_config.filter_nodes
653
self.excel_export_fields = export_config.fields
654
self.view = export_config.view
655
656
if ProjectFeature.DIFF in self.project_features:
657
if export_config.generate_diff_git is not None:
658
self.diff_git_revisions = export_config.generate_diff_git
659
if export_config.generate_diff_dirs is not None:
660
self.diff_dir_revisions = export_config.generate_diff_dirs
661
662
self.chromedriver = export_config.chromedriver
663
664
if (
665
export_config.enable_mathjax
666
and ProjectFeature.MATHJAX not in self.project_features
667
):668
self.project_features.append(ProjectFeature.MATHJAX)
669
670
if export_config.reqif_profile is not None:
671
self.reqif_profile = export_config.reqif_profile
672
673
# If the TOML file sets this to True, ignore what is in CLI.674
if not self.reqif_multiline_is_xhtml:
675
self.reqif_multiline_is_xhtml = (
676
export_config.reqif_multiline_is_xhtml
677
)678
if not self.reqif_enable_mid:
679
self.reqif_enable_mid = export_config.reqif_enable_mid
680
681
def validate_and_finalize(self) -> None:
682
project_path = self.get_project_root_path()
683
684
#685
# Validate source nodes config.686
#687
if (
688
len(self.source_nodes) > 0
689
and ProjectFeature.REQUIREMENT_TO_SOURCE_TRACEABILITY
690
not in self.project_features
691
):692
print( # noqa: T201
693
"warning: defining source_nodes without enabling REQUIREMENT_TO_SOURCE_TRACEABILITY "694
"has no effect"695
)696
697
if ProjectFeature.SOURCE_FILE_LANGUAGE_PARSERS in self.project_features:
698
print( # noqa: T201
699
"info: the SOURCE_FILE_LANGUAGE_PARSERS feature is no longer "700
"experimental and is now enabled by default. "701
"It can be safely removed from the project configuration."702
)703
704
if ProjectFeature.MATHJAX in self.project_features:
705
DEPRECATION_ENGINE.add_message(
706
"MATHJAX_feature_deprecated",
707
"The MATHJAX feature is now enabled by default and no longer "708
"needs to be listed in the project configuration. "709
"Please remove it from the project_features list in your "710
"strictdoc_config.py file.",
711
)712
713
if ProjectFeature.MERMAID in self.project_features:
714
DEPRECATION_ENGINE.add_message(
715
"MERMAID_feature_deprecated",
716
"The MERMAID feature is now enabled by default and no longer "717
"needs to be listed in the project configuration. "718
"Please remove it from the project_features list in your "719
"strictdoc_config.py file.",
720
)721
722
#723
# Validate HTML2PDF template path.724
#725
if (html2pdf_template := self.html2pdf_template) is not None:
726
assert not os.path.isabs(html2pdf_template)
727
if project_path is not None:
728
html2pdf_template = os.path.join(
729
project_path, html2pdf_template
730
)731
if not os.path.isfile(html2pdf_template):
732
raise ValueError(
733
"config: html2pdf_template: "734
f"invalid path to a template file: {html2pdf_template}."
735
)736
737
#738
# Validate favicon path.739
#740
if (favicon_path := self.favicon_path) is not None:
741
assert not os.path.isabs(favicon_path)
742
if project_path is not None:
743
favicon_path = os.path.join(project_path, favicon_path)
744
if not os.path.isfile(favicon_path):
745
raise ValueError(
746
"config: favicon_path: "747
f"invalid path to a favicon file: {favicon_path}."
748
)749
self.favicon_path = favicon_path
750
751
#752
# Validate custom CSS path.753
#754
if (custom_css_path := self.custom_css_path) is not None:
755
assert not os.path.isabs(custom_css_path)
756
if project_path is not None:
757
custom_css_path = os.path.join(project_path, custom_css_path)
758
if not os.path.isfile(custom_css_path):
759
raise ValueError(
760
"config: custom_css_path: "761
f"invalid path to a CSS file: {custom_css_path}."
762
)763
self.custom_css_path = custom_css_path
764
765
#766
# Validate path to Chrome Driver.767
#768
if (
769
chromedriver := self.chromedriver
770
) is not None and not os.path.isfile(chromedriver):
771
raise ValueError(
772
f"config: chromedriver: not found at path: {chromedriver}."
773
)774
775
#776
# Resolve the source root path.777
#778
if os.path.isdir(project_path):
779
source_root_path = self.source_root_path
780
if source_root_path is not None:
781
original_source_root_path = source_root_path
782
if not os.path.isabs(source_root_path):
783
source_root_path = os.path.join(
784
project_path, source_root_path
785
)786
source_root_path = os.path.abspath(source_root_path)
787
if not os.path.isdir(source_root_path):
788
raise ValueError(
789
"config: "790
"source_root_path: "791
f"Provided path does not exist: "
792
f"{original_source_root_path}."
793
)794
self.source_root_path = source_root_path
795
796
#797
# Read exclude paths from .gitignore. Add them to the user project's798
# both SDoc and source file search paths.799
#800
path_to_gitignore = os.path.join(project_path, ".gitignore")
801
if os.path.isfile(path_to_gitignore):
802
patterns = ["/.git/"]
803
804
with open(path_to_gitignore, encoding="utf-8") as f:
805
for line_ in f:
806
line = line_.strip()
807
if not line or line.startswith("#"):
808
continue809
# Ignore !-negated gitignores for now or reimplement810
# using a dedicated gitignore Python library.811
if line.startswith("!"):
812
continue813
patterns.append(line)
814
815
self.exclude_doc_paths.extend(patterns)
816
self.exclude_source_paths.extend(patterns)
817
818
#819
# Validate that the provided grammar shortcuts all point to existing820
# grammar files.821
#822
for grammar_alias_, grammar_path_ in list(self.grammars.items()):
823
assert grammar_alias_.startswith("@"), (
824
"Grammar alias must start with an '@' character."825
)826
assert "." not in grammar_alias_, (
827
"Grammar alias must not contain any . characters."828
)829
assert os.path.isfile(os.path.join(project_path, grammar_path_)), (
830
"Grammar path must point to an existing path relative to the "831
f"project config file: {grammar_path_}."
832
)833
if grammar_path_.startswith("./"):
834
self.grammars[grammar_alias_] = grammar_path_.removeprefix("./")
835
836
def is_feature_activated(self, feature: ProjectFeature) -> bool:
837
return feature in self.project_features
838
839
def get_favicon_variant(self) -> str:
840
return resolve_favicon_variant(
841
self.environment, self.is_running_on_server
842
)843
844
def get_custom_favicon_path(self) -> Optional[str]:
845
if self.favicon_path is None:
846
return None
847
if self.get_favicon_variant() in ("dev", "test"):
848
return None
849
return self.favicon_path
850
851
def get_favicon_filename(self) -> str:
852
custom_favicon_path = self.get_custom_favicon_path()
853
if custom_favicon_path is None:
854
return "favicon.svg"
855
return "favicon" + os.path.splitext(custom_favicon_path)[1]
856
857
def get_favicon_mime_type(self) -> str:
858
custom_favicon_path = self.get_custom_favicon_path()
859
if custom_favicon_path is None:
860
return "image/svg+xml"
861
mime_type, _ = mimetypes.guess_type(custom_favicon_path)
862
return mime_type or "application/octet-stream"
863
864
def get_custom_css_filename(self) -> str:
865
# The fixed name under which the custom CSS file (custom_css_path)866
# is copied to the static assets output directory and linked from867
# base.jinja.html. A fixed name avoids collisions with StrictDoc's868
# own stylesheets.869
return "custom.css"
870
871
def is_activated_table_screen(self) -> bool:
872
return ProjectFeature.TABLE_SCREEN in self.project_features
873
874
def is_activated_trace_screen(self) -> bool:
875
return ProjectFeature.TRACEABILITY_SCREEN in self.project_features
876
877
def is_activated_deep_trace_screen(self) -> bool:
878
return ProjectFeature.DEEP_TRACEABILITY_SCREEN in self.project_features
879
880
def is_activated_project_statistics(self) -> bool:
881
return (
882
self.get_feature(ProjectFeature.PROJECT_STATISTICS_SCREEN)
883
is not None
884
)885
886
def is_activated_requirements_to_source_traceability(self) -> bool:
887
return (
888
ProjectFeature.REQUIREMENT_TO_SOURCE_TRACEABILITY
889
in self.project_features
890
)891
892
def is_activated_requirements_coverage(self) -> bool:
893
return (
894
ProjectFeature.TRACEABILITY_MATRIX_SCREEN in self.project_features
895
)896
897
def is_activated_tree_map(self) -> bool:
898
return ProjectFeature.TREE_MAP_SCREEN in self.project_features
899
900
def is_activated_search(self) -> bool:
901
return (
902
self.is_running_on_server
903
and ProjectFeature.SEARCH in self.project_features
904
)905
906
def is_activated_html2pdf(self) -> bool:
907
return ProjectFeature.HTML2PDF in self.project_features
908
909
def is_activated_diff(self) -> bool:
910
return ProjectFeature.DIFF in self.project_features
911
912
def is_activated_reqif(self) -> bool:
913
return ProjectFeature.REQIF in self.project_features
914
915
def is_activated_mathjax(self) -> bool:
916
# FIXME: Refactor Jinja templates to not rely on the MathJax feature917
# flag, since MathJax is now a stable feature that is always included in918
# the static assets.919
return True
920
921
def is_activated_mermaid(self) -> bool:
922
# FIXME: Refactor Jinja templates to not rely on the Mermaid feature923
# flag, since Mermaid is now a stable feature that is always included924
# in the static assets.925
return True
926
927
def get_project_root_path(self) -> str:
928
if self.input_paths is not None and len(self.input_paths) > 0:
929
return self.input_paths[0]
930
raise NotImplementedError
931
932
def get_strictdoc_root_path(self) -> str:
933
return self.environment.path_to_strictdoc
934
935
def get_path_to_cache_dir(self) -> str:
936
return self.dir_for_sdoc_cache
937
938
def get_static_files_paths(self) -> List[str]:
939
return self.environment.get_static_files_paths()
940
941
def get_project_hash(self) -> str:
942
assert self.input_paths is not None and len(self.input_paths) > 0
943
return get_md5(self.input_paths[0])
944
945
def get_relevant_source_nodes_entry(
946
self, path_to_file: str
947
) -> Optional[SourceNodesEntry]:
948
"""
949
Get relevant source_nodes config item for a given source code file.950
951
Returns data for the first entry from source_nodes that is a parent path of path_to_file.952
If path_to_file is absolute, source node config entries are assumed to be in the source_root_path.953
"""954
955
source_root_path = self.source_root_path
956
assert source_root_path is not None
957
assert os.path.exists(source_root_path), source_root_path
958
959
source_file_path = Path(path_to_file)
960
for sdoc_source_config_entry_ in self.source_nodes:
961
# FIXME: Move the setting of full paths to .finalize() of this config962
# class when it is implemented.963
if sdoc_source_config_entry_.full_path is None:
964
sdoc_source_config_entry_.full_path = Path(
965
source_root_path966
) / Path(sdoc_source_config_entry_.path)
967
968
if source_file_path.is_absolute():
969
if (
970
sdoc_source_config_entry_.full_path
971
in source_file_path.parents
972
):973
return sdoc_source_config_entry_
974
else:
975
if (
976
Path(sdoc_source_config_entry_.path)
977
in source_file_path.parents
978
):979
return sdoc_source_config_entry_
980
981
return None
982
983
984
class ProjectConfigLoader:
985
@classmethod986
def load(
987
cls, input_path: str, output_dir: Optional[str] = None
988
) -> ProjectConfig:
989
assert os.path.exists(input_path), input_path
990
project_config: ProjectConfig = cls.load_from_path_or_get_default(
991
path_to_config=input_path
992
)993
project_config.input_paths = [input_path]
994
if output_dir is not None:
995
project_config.output_dir = output_dir
996
project_config.validate_and_finalize()
997
return project_config
998
999
@classmethod1000
def load_using_export_config(
1001
cls,
1002
export_config: ExportCommandConfig,
1003
) -> ProjectConfig:
1004
path_to_config = export_config.get_path_to_config()
1005
project_config: ProjectConfig = cls.load_from_path_or_get_default(
1006
path_to_config=path_to_config
1007
)1008
project_config.integrate_export_config(export_config)
1009
project_config.validate_and_finalize()
1010
return project_config
1011
1012
@classmethod1013
def load_using_server_config(
1014
cls,
1015
server_config: ServerCommandConfig,
1016
) -> ProjectConfig:
1017
path_to_config = server_config.get_path_to_config()
1018
project_config: ProjectConfig = cls.load_from_path_or_get_default(
1019
path_to_config=path_to_config
1020
)1021
project_config.integrate_server_config(server_config)
1022
project_config.validate_and_finalize()
1023
return project_config
1024
1025
@classmethod1026
def load_using_convert_config(
1027
cls,
1028
convert_config: ConvertCommandConfig,
1029
) -> ProjectConfig:
1030
path_to_config = convert_config.get_path_to_config()
1031
project_config: ProjectConfig = cls.load_from_path_or_get_default(
1032
path_to_config=path_to_config
1033
)1034
project_config.input_paths = [os.getcwd()]
1035
project_config.validate_and_finalize()
1036
return project_config
1037
1038
@classmethod1039
def load_using_manage_autouid_config(
1040
cls,
1041
manage_autouid_config: ManageAutoUIDCommandConfig,
1042
) -> ProjectConfig:
1043
path_to_config = manage_autouid_config.get_path_to_config()
1044
1045
project_config: ProjectConfig = cls.load_from_path_or_get_default(
1046
path_to_config=path_to_config
1047
)1048
1049
# FIXME: Encapsulate all this in project_config.integrate_manage_autouid_config(),1050
# following the example of integrate_export_config().1051
project_config.input_paths = [manage_autouid_config.input_path]
1052
if project_config.source_root_path is None:
1053
project_config.source_root_path = str(
1054
Path(manage_autouid_config.input_path).resolve()
1055
)1056
project_config.auto_uid_mode = True
1057
project_config.autouuid_include_sections = (
1058
manage_autouid_config.include_sections
1059
)1060
1061
# FIXME: Traceability Index is coupled with HTML output.1062
project_config.export_output_html_root = "NOT_RELEVANT"
1063
1064
project_config.validate_and_finalize()
1065
1066
return project_config
1067
1068
@classmethod1069
def load_using_format_config(
1070
cls,
1071
format_config: FormatCommandConfig,
1072
) -> "ProjectConfig":
1073
path_to_config = format_config.get_path_to_config()
1074
1075
project_config: ProjectConfig = cls.load_from_path_or_get_default(
1076
path_to_config=path_to_config
1077
)1078
1079
project_config.input_paths = [format_config.input_path]
1080
if project_config.source_root_path is None:
1081
project_config.source_root_path = str(
1082
Path(format_config.input_path).resolve()
1083
)1084
1085
# FIXME: Traceability Index is coupled with HTML output.1086
project_config.export_output_html_root = "NOT_RELEVANT"
1087
1088
project_config.validate_and_finalize()
1089
1090
return project_config
1091
1092
@classmethod1093
def load_using_manage_new_config(
1094
cls,
1095
manage_new_config: ManageNewCommandConfig,
1096
) -> "ProjectConfig":
1097
path_to_config = manage_new_config.get_path_to_config()
1098
1099
project_config: ProjectConfig = cls.load_from_path_or_get_default(
1100
path_to_config=path_to_config
1101
)1102
1103
project_config.input_paths = [manage_new_config.project_root_path]
1104
if project_config.source_root_path is None:
1105
project_config.source_root_path = str(
1106
Path(manage_new_config.project_root_path).resolve()
1107
)1108
1109
# FIXME: Traceability Index is coupled with HTML output.1110
project_config.export_output_html_root = "NOT_RELEVANT"
1111
1112
project_config.validate_and_finalize()
1113
1114
return project_config
1115
1116
@staticmethod1117
def load_from_path_or_get_default(
1118
*,
1119
path_to_config: str,
1120
) -> ProjectConfig:
1121
if not os.path.exists(path_to_config):
1122
return ProjectConfig.default_config()
1123
if os.path.isdir(path_to_config):
1124
path_to_config_dir = path_to_config
1125
# Prefer the Python config file when both are present.1126
path_to_py_config = os.path.join(
1127
path_to_config_dir, "strictdoc_config.py"
1128
)1129
path_to_toml_config = os.path.join(
1130
path_to_config_dir, "strictdoc.toml"
1131
)1132
1133
if os.path.isfile(path_to_py_config):
1134
path_to_config = path_to_py_config
1135
elif os.path.isfile(path_to_toml_config):
1136
path_to_config = path_to_toml_config
1137
1138
if not os.path.isfile(path_to_config):
1139
return ProjectConfig.default_config()
1140
1141
if path_to_config.endswith(".py"):
1142
return ProjectConfigLoader.load_from_python(
1143
config_py_path=path_to_config
1144
)1145
1146
try:
1147
config_content = toml.load(path_to_config)
1148
except toml.decoder.TomlDecodeError as exception:
1149
raise StrictDocException( # noqa: T201
1150
f"Could not parse the config file {path_to_config}: "
1151
f"{exception}."
1152
) from None
1153
except Exception as exception: # pragma: no cover
1154
raise AssertionError from exception
1155
1156
DEPRECATION_ENGINE.add_message(
1157
"DEPRECATED_CONFIG_TOML",
1158
(1159
"WARNING: StrictDoc TOML configuration format is deprecated. "1160
"Replace the TOML config file with a Python config file.\n\n"
1161
"See the migration guide for mode details:\n\n"
1162
"https://strictdoc.readthedocs.io/en/stable/?a=SECTION-UG-MIGRATE-CONFIG-2025-Q4"1163
),1164
)1165
1166
config_last_update = get_file_modification_time(path_to_config)
1167
1168
return ProjectConfigLoader._load_from_dictionary(
1169
config_dict=config_content,
1170
config_last_update=config_last_update,
1171
)1172
1173
@staticmethod1174
def load_from_python(*, config_py_path: str) -> ProjectConfig:
1175
module = import_from_path(config_py_path)
1176
create_config_function = module.create_config
1177
assert isinstance(create_config_function, types.FunctionType), type(
1178
create_config_function1179
)1180
project_config = create_config_function()
1181
assert isinstance(project_config, ProjectConfig)
1182
project_config.config_last_update = get_file_modification_time(
1183
config_py_path1184
)1185
return project_config
1186
1187
@staticmethod1188
def _load_from_dictionary(
1189
*,
1190
config_dict: Dict[str, Any],
1191
config_last_update: Optional[datetime.datetime],
1192
) -> ProjectConfig:
1193
project_title = ProjectConfigDefault.DEFAULT_PROJECT_TITLE
1194
dir_for_sdoc_assets = ProjectConfigDefault.DEFAULT_DIR_FOR_SDOC_ASSETS
1195
dir_for_sdoc_cache = ProjectConfigDefault.DEFAULT_DIR_FOR_SDOC_CACHE
1196
project_features: List[Union[str, Feature]] = list(
1197
ProjectConfigDefault.DEFAULT_FEATURES
1198
)1199
server_host = ProjectConfigDefault.DEFAULT_SERVER_HOST
1200
server_port = ProjectConfigDefault.DEFAULT_SERVER_PORT
1201
include_doc_paths: List[str] = []
1202
exclude_doc_paths: List[str] = []
1203
source_root_path = None
1204
include_source_paths: List[str] = []
1205
exclude_source_paths: List[str] = []
1206
test_report_root_dict: Dict[str, str] = {}
1207
source_nodes: List[SourceNodesEntry] = []
1208
html2pdf_strict: bool = False
1209
html2pdf_template: Optional[str] = None
1210
custom_css_path: Optional[str] = None
1211
bundle_document_version = (
1212
ProjectConfigDefault.DEFAULT_BUNDLE_DOCUMENT_VERSION
1213
)1214
bundle_document_date = (
1215
ProjectConfigDefault.DEFAULT_BUNDLE_DOCUMENT_COMMIT_DATE
1216
)1217
1218
traceability_matrix_relation_columns: Optional[
1219
List[Tuple[str, Optional[str]]]
1220
] = None
1221
reqif_profile = ReqIFProfile.P01_SDOC
1222
reqif_multiline_is_xhtml = False
1223
reqif_enable_mid = False
1224
reqif_import_markup: Optional[str] = None
1225
chromedriver: Optional[str] = None
1226
1227
section_behavior: str = ProjectConfigDefault.DEFAULT_SECTION_BEHAVIOR
1228
statistics_generator: Optional[str] = None
1229
document_line_width: Optional[int] = None
1230
1231
if "project" in config_dict:
1232
project_content = config_dict["project"]
1233
project_title = project_content.get("title", project_title)
1234
dir_for_sdoc_assets = project_content.get(
1235
"html_assets_strictdoc_dir", dir_for_sdoc_assets
1236
)1237
dir_for_sdoc_cache = project_content.get(
1238
"cache_dir", dir_for_sdoc_cache
1239
)1240
1241
project_features = project_content.get("features", project_features)
1242
1243
statistics_generator = project_content.get(
1244
"statistics_generator", statistics_generator
1245
)1246
1247
include_doc_paths = project_content.get(
1248
"include_doc_paths", include_doc_paths
1249
)1250
1251
exclude_doc_paths = project_content.get(
1252
"exclude_doc_paths", exclude_doc_paths
1253
)1254
1255
source_root_path = project_content.get(
1256
"source_root_path", source_root_path
1257
)1258
1259
include_source_paths = project_content.get(
1260
"include_source_paths", include_source_paths
1261
)1262
1263
exclude_source_paths = project_content.get(
1264
"exclude_source_paths", exclude_source_paths
1265
)1266
1267
html2pdf_strict = project_content.get(
1268
"html2pdf_strict", html2pdf_strict
1269
)1270
1271
html2pdf_template = project_content.get(
1272
"html2pdf_template", html2pdf_template
1273
)1274
1275
custom_css_path = project_content.get(
1276
"custom_css_path", custom_css_path
1277
)1278
1279
bundle_document_version = project_content.get(
1280
"bundle_document_version", bundle_document_version
1281
)1282
1283
bundle_document_date = project_content.get(
1284
"bundle_document_date", bundle_document_date
1285
)1286
1287
traceability_matrix_relation_columns_config: Optional[List[str]] = (
1288
project_content.get(
1289
"traceability_matrix_relation_columns", None
1290
)1291
)1292
if traceability_matrix_relation_columns_config is not None:
1293
assert isinstance(
1294
traceability_matrix_relation_columns_config, list
1295
)1296
traceability_matrix_relation_columns = []
1297
for (
1298
relation_column_string_1299
) in traceability_matrix_relation_columns_config:
1300
relation_tuple = parse_relation_tuple(
1301
relation_column_string_1302
)1303
assert relation_tuple is not None
1304
traceability_matrix_relation_columns.append(relation_tuple)
1305
1306
chromedriver = project_content.get("chromedriver", chromedriver)
1307
1308
if (
1309
test_report_root_dict_ := project_content.get(
1310
"test_report_root_dict", None
1311
)1312
) is not None:
1313
assert isinstance(test_report_root_dict_, list), (
1314
test_report_root_dict1315
)1316
for test_report_root_entry_ in test_report_root_dict_:
1317
assert isinstance(test_report_root_entry_, dict)
1318
test_report_root_dict.update(test_report_root_entry_)
1319
1320
section_behavior = project_content.get(
1321
"section_behavior", section_behavior
1322
)1323
assert section_behavior in ("[SECTION]", "[[SECTION]]")
1324
1325
document_line_width = project_content.get(
1326
"document_line_width", document_line_width
1327
)1328
1329
if "source_nodes" in project_content:
1330
source_nodes_config = project_content["source_nodes"]
1331
assert isinstance(source_nodes_config, list)
1332
for item_ in source_nodes_config:
1333
source_node_path = next(iter(item_))
1334
source_node_item = item_[source_node_path]
1335
source_nodes.append(
1336
SourceNodesEntry(
1337
path=source_node_path,
1338
uid=source_node_item["uid"],
1339
node_type=source_node_item["node_type"],
1340
sdoc_to_source_map=source_node_item["map"]
1341
if "map" in source_node_item
1342
else {},
1343
)1344
)1345
1346
if "server" in config_dict:
1347
server_content = config_dict["server"]
1348
server_host = server_content.get("host", server_host)
1349
server_port = server_content.get("port", server_port)
1350
1351
if "reqif" in config_dict:
1352
reqif_content = config_dict["reqif"]
1353
reqif_multiline_is_xhtml = reqif_content.get(
1354
"multiline_is_xhtml", False
1355
)1356
reqif_enable_mid = reqif_content.get("enable_mid", False)
1357
reqif_import_markup = reqif_content.get("import_markup", None)
1358
1359
return ProjectConfig(
1360
project_title=project_title,
1361
dir_for_sdoc_assets=dir_for_sdoc_assets,
1362
dir_for_sdoc_cache=dir_for_sdoc_cache,
1363
project_features=project_features,
1364
server_host=server_host,
1365
server_port=server_port,
1366
include_doc_paths=include_doc_paths,
1367
exclude_doc_paths=exclude_doc_paths,
1368
source_root_path=source_root_path,
1369
include_source_paths=include_source_paths,
1370
exclude_source_paths=exclude_source_paths,
1371
test_report_root_dict=test_report_root_dict,
1372
source_nodes=source_nodes,
1373
html2pdf_strict=html2pdf_strict,
1374
html2pdf_template=html2pdf_template,
1375
custom_css_path=custom_css_path,
1376
bundle_document_version=bundle_document_version,
1377
bundle_document_date=bundle_document_date,
1378
traceability_matrix_relation_columns=traceability_matrix_relation_columns,
1379
reqif_profile=reqif_profile,
1380
reqif_multiline_is_xhtml=reqif_multiline_is_xhtml,
1381
reqif_enable_mid=reqif_enable_mid,
1382
reqif_import_markup=reqif_import_markup,
1383
chromedriver=chromedriver,
1384
section_behavior=section_behavior,
1385
statistics_generator=statistics_generator,
1386
document_line_width=document_line_width,
1387
_config_last_update=config_last_update,
1388
)