Path:
strictdoc/core/project_config.py
Lines:
1083
Non-empty lines:
925
Non-empty lines covered with requirements:
925 / 925 (100.0%)
Functions:
43
Functions covered by requirements:
43 / 43 (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 os
7
import re
8
import tempfile
9
import types
10
from dataclasses import dataclass, field
11
from enum import Enum
12
from pathlib import Path
13
from typing import Any, Dict, List, Optional, Tuple
14
15
import toml
16
17
from strictdoc import __version__, environment
18
from strictdoc.backend.reqif.sdoc_reqif_fields import ReqIFProfile
19
from strictdoc.backend.sdoc.constants import SDocMarkup
20
from strictdoc.commands.export_config import ExportCommandConfig
21
from strictdoc.commands.import_excel_config import ImportExcelCommandConfig
22
from strictdoc.commands.import_reqif_config import ImportReqIFCommandConfig
23
from strictdoc.commands.manage_autouid_config import ManageAutoUIDCommandConfig
24
from strictdoc.commands.server_config import ServerCommandConfig
25
from strictdoc.core.environment import SDocRuntimeEnvironment
26
from strictdoc.core.plugin import StrictDocPlugin
27
from strictdoc.helpers.auto_described import auto_described
28
from strictdoc.helpers.deprecation_engine import DEPRECATION_ENGINE
29
from strictdoc.helpers.exception import StrictDocException
30
from strictdoc.helpers.file_modification_time import get_file_modification_time
31
from strictdoc.helpers.md5 import get_md5
32
from strictdoc.helpers.module import import_from_path
33
from strictdoc.helpers.net import is_valid_host
34
from strictdoc.helpers.path_filter import validate_mask
35
36
37
def parse_relation_tuple(column_name: str) -> Optional[Tuple[str, str]]:
38
match_result = re.search(
39
r"^((Parent|Child|File)?)(\[(.{1,32})])?$", column_name
40
)41
if match_result is None:
42
return None
43
return match_result.group(1), match_result.group(4)
44
45
46
@dataclass47
class SourceNodesEntry:
48
path: str
49
uid: str
50
node_type: str
51
sdoc_to_source_map: Dict[str, str] = field(default_factory=dict)
52
full_path: Optional[Path] = None
53
54
55
class ProjectFeature(str, Enum):
56
# Stable features.57
TABLE_SCREEN = "TABLE_SCREEN"
58
TRACEABILITY_SCREEN = "TRACEABILITY_SCREEN"
59
DEEP_TRACEABILITY_SCREEN = "DEEP_TRACEABILITY_SCREEN"
60
61
MATHJAX = "MATHJAX"
62
63
# Experimental features.64
SEARCH = "SEARCH"
65
HTML2PDF = "HTML2PDF"
66
REQIF = "REQIF"
67
DIFF = "DIFF"
68
PROJECT_STATISTICS_SCREEN = "PROJECT_STATISTICS_SCREEN"
69
TREE_MAP_SCREEN = "TREE_MAP_SCREEN"
70
TRACEABILITY_MATRIX_SCREEN = "TRACEABILITY_MATRIX_SCREEN"
71
REQUIREMENT_TO_SOURCE_TRACEABILITY = "REQUIREMENT_TO_SOURCE_TRACEABILITY"
72
SOURCE_FILE_LANGUAGE_PARSERS = "SOURCE_FILE_LANGUAGE_PARSERS"
73
74
MERMAID = "MERMAID"
75
RAPIDOC = "RAPIDOC"
76
NESTOR = "NESTOR"
77
78
ALL_FEATURES = "ALL_FEATURES"
79
80
@staticmethod81
def all() -> List[str]: # noqa: A003
82
return list(map(lambda c: c.value, ProjectFeature))
83
84
85
class ProjectConfigDefault:
86
DEFAULT_PROJECT_TITLE = "Untitled Project"
87
DEFAULT_DIR_FOR_SDOC_ASSETS = "_static"
88
DEFAULT_DIR_FOR_OUTPUT = "output"
89
DEFAULT_DIR_FOR_SDOC_CACHE = "output/_cache"
90
91
DEFAULT_FEATURES: List[str] = [
92
ProjectFeature.TABLE_SCREEN,
93
ProjectFeature.TRACEABILITY_SCREEN,
94
ProjectFeature.DEEP_TRACEABILITY_SCREEN,
95
ProjectFeature.SEARCH,
96
]97
DEFAULT_SERVER_HOST = "127.0.0.1"
98
DEFAULT_SERVER_PORT = 5111
99
DEFAULT_BUNDLE_DOCUMENT_VERSION = "@GIT_VERSION (Git branch: @GIT_BRANCH)"
100
DEFAULT_BUNDLE_DOCUMENT_COMMIT_DATE = "@GIT_COMMIT_DATETIME"
101
DEFAULT_SECTION_BEHAVIOR = "[SECTION]"
102
103
104
@auto_described105
class ProjectConfig:
106
"""
107
@relation(SDOC-SRS-119, scope=class)108
"""109
110
def __init__(
111
self,
112
project_title: str = ProjectConfigDefault.DEFAULT_PROJECT_TITLE,
113
dir_for_sdoc_assets: str = ProjectConfigDefault.DEFAULT_DIR_FOR_SDOC_ASSETS,
114
dir_for_sdoc_cache: str = ProjectConfigDefault.DEFAULT_DIR_FOR_SDOC_CACHE,
115
project_features: Optional[List[str]] = None,
116
server_host: str = ProjectConfigDefault.DEFAULT_SERVER_HOST,
117
server_port: int = ProjectConfigDefault.DEFAULT_SERVER_PORT,
118
input_paths: Optional[List[str]] = None,
119
include_doc_paths: Optional[List[str]] = None,
120
exclude_doc_paths: Optional[List[str]] = None,
121
source_root_path: Optional[str] = None,
122
include_source_paths: Optional[List[str]] = None,
123
exclude_source_paths: Optional[List[str]] = None,
124
grammars: Optional[Dict[str, str]] = None,
125
test_report_root_dict: Optional[Dict[str, str]] = None,
126
source_nodes: Optional[List[SourceNodesEntry]] = None,
127
html2pdf_strict: bool = False,
128
html2pdf_template: Optional[str] = None,
129
html2pdf_forced_page_break_nodes: Optional[List[str]] = None,
130
bundle_document_uid: Optional[str] = None,
131
bundle_document_version: Optional[
132
str133
] = ProjectConfigDefault.DEFAULT_BUNDLE_DOCUMENT_VERSION,
134
bundle_document_date: Optional[
135
str136
] = ProjectConfigDefault.DEFAULT_BUNDLE_DOCUMENT_COMMIT_DATE,
137
traceability_matrix_relation_columns: Optional[
138
List[Tuple[str, Optional[str]]]
139
] = None,
140
reqif_profile: str = ReqIFProfile.P01_SDOC,
141
# FIXME: Change to true by default.142
reqif_multiline_is_xhtml: bool = False,
143
# FIXME: Change to true by default.144
reqif_enable_mid: bool = False,
145
reqif_import_markup: Optional[str] = None,
146
diff_git_revisions: Optional[str] = None,
147
diff_dir_revisions: Optional[Tuple[str, str]] = None,
148
chromedriver: Optional[str] = None,
149
# FIXME: The section_behavior field will be removed by the end of 2025-Q4.150
section_behavior: Optional[
151
str152
] = ProjectConfigDefault.DEFAULT_SECTION_BEHAVIOR,
153
statistics_generator: Optional[str] = None,
154
# Logo path can be set in the project config to customize the launcher's appearance for a specific project.155
launcher_logo_path: Optional[str] = None,
156
user_plugin: Optional[StrictDocPlugin] = None,
157
# Reserved for StrictDoc's internal use.158
_config_last_update: Optional[datetime.datetime] = None,
159
) -> None:
160
self.environment: SDocRuntimeEnvironment = environment
161
162
# Settings obtained from the strictdoc.toml config file.163
self.project_title: str = project_title
164
self.dir_for_sdoc_assets: str = dir_for_sdoc_assets
165
166
if env_cache_dir := os.environ.get("STRICTDOC_CACHE_DIR"):
167
# The only use case for STRICTDOC_CACHE_DIR is to make the cache168
# local to an itest folder.169
assert env_cache_dir == "Output/_cache", env_cache_dir
170
dir_for_sdoc_cache = env_cache_dir
171
elif dir_for_sdoc_cache == "$TMPDIR":
172
dir_for_sdoc_cache = os.path.join(
173
tempfile.gettempdir(),
174
"strictdoc_cache",
175
get_md5(os.getcwd()),
176
)177
178
# Adding a __version__ part to the cache directory improves traceability179
# by indicating which StrictDoc version the cache belongs to.180
# This helps prevent issues when switching between versions that may use181
# incompatible cache schemas.182
dir_for_sdoc_cache = os.path.join(dir_for_sdoc_cache, __version__)
183
184
self.dir_for_sdoc_cache: str = dir_for_sdoc_cache
185
186
#187
# project_features188
#189
project_features = (
190
project_features191
if project_features is not None
192
else ProjectConfigDefault.DEFAULT_FEATURES
193
)194
195
assert isinstance(project_features, list), (
196
f"config: project_features: parameter must be an "
197
f"array: '{project_features}'."
198
)199
200
for feature in project_features:
201
assert feature in ProjectFeature.all(), (
202
f"config: project_features: unknown feature declared: "
203
f"'{feature}'."
204
)205
206
if ProjectFeature.ALL_FEATURES in project_features:
207
project_features = ProjectFeature.all()
208
209
self.project_features: List[str] = project_features
210
211
#212
# server_host and server_port213
#214
assert is_valid_host(server_host), (
215
f"config: server_host: invalid host: {server_host}'."
216
)217
self.server_host: str = server_host
218
219
assert isinstance(server_port, int) and 1024 < server_port < 65000, (
220
f"strictdoc.toml: 'port': invalid port: {server_port}'."
221
)222
self.server_port: int = server_port
223
224
#225
# input_paths226
#227
self.input_paths: Optional[List[str]] = input_paths
228
229
#230
# include_doc_paths231
#232
include_doc_paths = include_doc_paths or []
233
assert isinstance(include_doc_paths, list), include_doc_paths
234
for include_doc_path in include_doc_paths:
235
try:
236
validate_mask(include_doc_path)
237
except SyntaxError as exception_:
238
raise ValueError(
239
f"config: include_doc_paths: {exception_}"
240
) from exception_
241
self.include_doc_paths: List[str] = include_doc_paths
242
243
#244
# exclude_doc_paths245
#246
exclude_doc_paths = exclude_doc_paths or []
247
assert isinstance(exclude_doc_paths, list), exclude_doc_paths
248
for exclude_doc_path in exclude_doc_paths:
249
try:
250
validate_mask(exclude_doc_path)
251
except SyntaxError as exception_:
252
raise ValueError(
253
f"config: exclude_doc_paths: {exception_}"
254
) from exception_
255
self.exclude_doc_paths: List[str] = exclude_doc_paths
256
257
#258
# include_source_paths259
#260
include_source_paths = include_source_paths or []
261
assert isinstance(include_source_paths, list), include_source_paths
262
for include_source_path in include_source_paths:
263
try:
264
validate_mask(include_source_path)
265
except SyntaxError as exception_:
266
raise ValueError(
267
f"config: include_source_paths: {exception_}"
268
) from exception_
269
self.include_source_paths: List[str] = include_source_paths
270
271
#272
# exclude_source_paths273
#274
exclude_source_paths = exclude_source_paths or []
275
assert isinstance(exclude_source_paths, list), exclude_source_paths
276
for exclude_source_path in exclude_source_paths:
277
try:
278
validate_mask(exclude_source_path)
279
except SyntaxError as exception_:
280
raise ValueError(
281
f"config: exclude_source_paths: {exception_}"
282
) from exception_
283
self.exclude_source_paths: List[str] = exclude_source_paths
284
285
#286
# source_root_path287
#288
self.source_root_path: Optional[str] = source_root_path
289
290
#291
# grammars - Grammar aliases.292
#293
self.grammars: Dict[str, str] = grammars or {}
294
295
self.test_report_root_dict: Dict[str, str] = (
296
test_report_root_dict if test_report_root_dict is not None else {}
297
)298
self.source_nodes: List[SourceNodesEntry] = (
299
source_nodes if source_nodes is not None else []
300
)301
302
# Settings derived from the command-line parameters.303
304
# Common settings.305
self.output_dir: str = ProjectConfigDefault.DEFAULT_DIR_FOR_OUTPUT
306
307
# Export action.308
self.export_output_html_root: str = os.path.join(
309
self.output_dir, "html"
310
)311
self.export_formats: Optional[List[str]] = None
312
self.export_included_documents: bool = False
313
self.generate_bundle_document: bool = False
314
self.filter_nodes: Optional[str] = None
315
316
self.excel_export_fields: Optional[List[str]] = None
317
318
assert isinstance(html2pdf_strict, bool), (
319
"config: html2pdf_strict: "320
f"must be a True/False value: {html2pdf_strict}."
321
)322
self.html2pdf_strict: bool = html2pdf_strict
323
324
self.html2pdf_template: Optional[str] = html2pdf_template
325
326
if html2pdf_forced_page_break_nodes is not None:
327
assert isinstance(html2pdf_forced_page_break_nodes, list)
328
assert len(html2pdf_forced_page_break_nodes) <= 10
329
self.html2pdf_forced_page_break_nodes: List[str] = (
330
html2pdf_forced_page_break_nodes or []
331
)332
333
self.bundle_document_uid: Optional[str] = bundle_document_uid
334
self.bundle_document_version: Optional[str] = bundle_document_version
335
self.bundle_document_date: Optional[str] = bundle_document_date
336
337
self.traceability_matrix_relation_columns: Optional[
338
List[Tuple[str, Optional[str]]]
339
] = traceability_matrix_relation_columns
340
341
#342
# ReqIF343
#344
self.reqif_profile: str = reqif_profile
345
346
assert isinstance(reqif_multiline_is_xhtml, bool), (
347
reqif_multiline_is_xhtml348
)349
self.reqif_multiline_is_xhtml: bool = reqif_multiline_is_xhtml
350
351
assert isinstance(reqif_enable_mid, bool), reqif_enable_mid
352
self.reqif_enable_mid: bool = reqif_enable_mid
353
354
if reqif_import_markup is not None:
355
assert reqif_import_markup in SDocMarkup.ALL, (
356
"config: reqif_import_markup: expected a valid markup: "357
f"({SDocMarkup.ALL}). Got: "
358
f"'{reqif_import_markup}'."
359
)360
361
self.reqif_import_markup: Optional[str] = reqif_import_markup
362
363
#364
# auto_uid_mode: default is False. The True-case is used by the365
# manage/auto_uid command: the SDocNodeValidator will366
# not raise an exception if it sees a node with a missing UID.367
# Important for a special case:368
# The Manage UID command auto-generates the UID, so the field presence369
# validation has to be relaxed.370
# The GitHub issue report:371
# manage auto-uid: UID field REQUIRED True leads to an error372
# https://github.com/strictdoc-project/strictdoc/issues/1896373
#374
self.auto_uid_mode = False
375
self.autouuid_include_sections: bool = False
376
377
self.view: Optional[str] = None
378
379
self.diff_git_revisions: Optional[str] = diff_git_revisions
380
self.diff_dir_revisions: Optional[Tuple[str, str]] = diff_dir_revisions
381
382
self.chromedriver: Optional[str] = chromedriver
383
self.section_behavior: Optional[str] = section_behavior
384
385
self.statistics_generator: Optional[str] = statistics_generator
386
self.user_plugin: Optional[StrictDocPlugin] = user_plugin
387
388
# Optional launcher logo path (absolute or workspace-relative).389
self.launcher_logo_path: Optional[str] = launcher_logo_path
390
391
self.config_last_update: Optional[datetime.datetime] = (
392
_config_last_update393
)394
self.is_running_on_server: bool = False
395
396
@staticmethod397
def default_config() -> "ProjectConfig":
398
return ProjectConfig()
399
400
# Some server command settings can override the project config settings.401
def integrate_server_config(
402
self, server_config: ServerCommandConfig
403
) -> None:
404
self.is_running_on_server = True
405
if (server_host_ := server_config.host) is not None:
406
self.server_host = server_host_
407
if (server_port_ := server_config.port) is not None:
408
self.server_port = server_port_
409
410
self.input_paths = [server_config.get_full_input_path()]
411
if self.source_root_path is None:
412
source_root_path = self.input_paths[0]
413
# If the input argument is a relative path, convert it to an414
# absolute path.415
source_root_path = os.path.abspath(source_root_path)
416
source_root_path = source_root_path.rstrip("/")
417
self.source_root_path = source_root_path
418
419
# When setting the output dir, the CLI argument takes precedence.420
output_dir = self.output_dir
421
if server_config.output_path is not None:
422
output_dir = server_config.output_path
423
elif output_dir == ProjectConfigDefault.DEFAULT_DIR_FOR_OUTPUT:
424
output_dir = "./output/server"
425
self.output_dir = output_dir
426
427
self.export_output_html_root = os.path.join(output_dir, "html")
428
429
# If a custom cache folder is not specified in the config, adjust the430
# cache folder to be located in the output folder.431
if self.dir_for_sdoc_cache.startswith(
432
ProjectConfigDefault.DEFAULT_DIR_FOR_SDOC_CACHE
433
):434
self.dir_for_sdoc_cache = os.path.join(
435
output_dir, "_cache", __version__
436
)437
438
self.export_formats = ["html"]
439
self.generate_bundle_document = False
440
self.export_included_documents = True
441
442
def integrate_export_config(
443
self, export_config: ExportCommandConfig
444
) -> None:
445
if export_config.project_title is not None:
446
self.project_title = export_config.project_title
447
448
self.input_paths = export_config.input_paths
449
if self.source_root_path is None:
450
source_root_path = export_config.input_paths[0]
451
# If the input argument is a relative path, convert it to an452
# absolute path.453
source_root_path = os.path.abspath(source_root_path)
454
source_root_path = source_root_path.rstrip("/")
455
self.source_root_path = source_root_path
456
457
#458
# Adjust the default output dir to the user-provided dir if needed.459
#460
output_dir = self.output_dir
461
if export_config.output_dir is not None:
462
output_dir = export_config.output_dir
463
if not os.path.isabs(output_dir):
464
cwd = os.getcwd()
465
output_dir = os.path.join(cwd, output_dir)
466
self.output_dir = output_dir
467
468
# If a custom cache folder is not specified in the config, adjust the469
# cache folder to be located in the output folder.470
if self.dir_for_sdoc_cache.startswith(
471
ProjectConfigDefault.DEFAULT_DIR_FOR_SDOC_CACHE
472
):473
self.dir_for_sdoc_cache = os.path.join(
474
output_dir, "_cache", __version__
475
)476
477
self.export_output_html_root = os.path.join(self.output_dir, "html")
478
self.export_formats = export_config.formats
479
self.export_included_documents = export_config.included_documents
480
self.generate_bundle_document = export_config.generate_bundle_document
481
self.filter_nodes = export_config.filter_nodes
482
self.excel_export_fields = export_config.fields
483
self.view = export_config.view
484
485
if ProjectFeature.DIFF in self.project_features:
486
if export_config.generate_diff_git is not None:
487
self.diff_git_revisions = export_config.generate_diff_git
488
if export_config.generate_diff_dirs is not None:
489
self.diff_dir_revisions = export_config.generate_diff_dirs
490
491
self.chromedriver = export_config.chromedriver
492
493
if (
494
export_config.enable_mathjax
495
and ProjectFeature.MATHJAX not in self.project_features
496
):497
self.project_features.append(ProjectFeature.MATHJAX)
498
499
if export_config.reqif_profile is not None:
500
self.reqif_profile = export_config.reqif_profile
501
502
# If the TOML file sets this to True, ignore what is in CLI.503
if not self.reqif_multiline_is_xhtml:
504
self.reqif_multiline_is_xhtml = (
505
export_config.reqif_multiline_is_xhtml
506
)507
if not self.reqif_enable_mid:
508
self.reqif_enable_mid = export_config.reqif_enable_mid
509
510
def validate_and_finalize(self) -> None:
511
project_path = self.get_project_root_path()
512
513
#514
# Validate source nodes config.515
#516
if (
517
len(self.source_nodes) > 0
518
and ProjectFeature.REQUIREMENT_TO_SOURCE_TRACEABILITY
519
not in self.project_features
520
):521
print( # noqa: T201
522
"warning: defining source_nodes without enabling REQUIREMENT_TO_SOURCE_TRACEABILITY "523
"has no effect"524
)525
526
if ProjectFeature.SOURCE_FILE_LANGUAGE_PARSERS in self.project_features:
527
print( # noqa: T201
528
"info: the SOURCE_FILE_LANGUAGE_PARSERS feature is no longer "529
"experimental and is now enabled by default. "530
"It can be safely removed from the project configuration."531
)532
533
#534
# Validate HTML2PDF template path.535
#536
if (html2pdf_template := self.html2pdf_template) is not None:
537
assert not os.path.isabs(html2pdf_template)
538
if project_path is not None:
539
html2pdf_template = os.path.join(
540
project_path, html2pdf_template
541
)542
if not os.path.isfile(html2pdf_template):
543
raise ValueError(
544
"config: html2pdf_template: "545
f"invalid path to a template file: {html2pdf_template}."
546
)547
548
#549
# Validate path to Chrome Driver.550
#551
if (
552
chromedriver := self.chromedriver
553
) is not None and not os.path.isfile(chromedriver):
554
raise ValueError(
555
f"config: chromedriver: not found at path: {chromedriver}."
556
)557
558
#559
# Resolve the source root path.560
#561
if os.path.isdir(project_path):
562
source_root_path = self.source_root_path
563
if source_root_path is not None:
564
original_source_root_path = source_root_path
565
if not os.path.isabs(source_root_path):
566
source_root_path = os.path.join(
567
project_path, source_root_path
568
)569
source_root_path = os.path.abspath(source_root_path)
570
if not os.path.isdir(source_root_path):
571
raise ValueError(
572
"config: "573
"source_root_path: "574
f"Provided path does not exist: "
575
f"{original_source_root_path}."
576
)577
self.source_root_path = source_root_path
578
579
#580
# Read exclude paths from .gitignore. Add them to the user project's581
# both SDoc and source file search paths.582
#583
path_to_gitignore = os.path.join(project_path, ".gitignore")
584
if os.path.isfile(path_to_gitignore):
585
patterns = ["/.git/"]
586
587
with open(path_to_gitignore, encoding="utf-8") as f:
588
for line_ in f:
589
line = line_.strip()
590
if not line or line.startswith("#"):
591
continue592
# Ignore !-negated gitignores for now or reimplement593
# using a dedicated gitignore Python library.594
if line.startswith("!"):
595
continue596
patterns.append(line)
597
598
self.exclude_doc_paths.extend(patterns)
599
self.exclude_source_paths.extend(patterns)
600
601
#602
# Validate that the provided grammar shortcuts all point to existing603
# grammar files.604
#605
for grammar_alias_, grammar_path_ in list(self.grammars.items()):
606
assert grammar_alias_.startswith("@"), (
607
"Grammar alias must start with an '@' character."608
)609
assert "." not in grammar_alias_, (
610
"Grammar alias must not contain any . characters."611
)612
assert os.path.isfile(os.path.join(project_path, grammar_path_)), (
613
"Grammar path must point to an existing path relative to the "614
f"project config file: {grammar_path_}."
615
)616
if grammar_path_.startswith("./"):
617
self.grammars[grammar_alias_] = grammar_path_.removeprefix("./")
618
619
def is_feature_activated(self, feature: ProjectFeature) -> bool:
620
return feature in self.project_features
621
622
def is_activated_table_screen(self) -> bool:
623
return ProjectFeature.TABLE_SCREEN in self.project_features
624
625
def is_activated_trace_screen(self) -> bool:
626
return ProjectFeature.TRACEABILITY_SCREEN in self.project_features
627
628
def is_activated_deep_trace_screen(self) -> bool:
629
return ProjectFeature.DEEP_TRACEABILITY_SCREEN in self.project_features
630
631
def is_activated_project_statistics(self) -> bool:
632
return ProjectFeature.PROJECT_STATISTICS_SCREEN in self.project_features
633
634
def is_activated_requirements_to_source_traceability(self) -> bool:
635
return (
636
ProjectFeature.REQUIREMENT_TO_SOURCE_TRACEABILITY
637
in self.project_features
638
)639
640
def is_activated_requirements_coverage(self) -> bool:
641
return (
642
ProjectFeature.TRACEABILITY_MATRIX_SCREEN in self.project_features
643
)644
645
def is_activated_tree_map(self) -> bool:
646
return ProjectFeature.TREE_MAP_SCREEN in self.project_features
647
648
def is_activated_search(self) -> bool:
649
return (
650
self.is_running_on_server
651
and ProjectFeature.SEARCH in self.project_features
652
)653
654
def is_activated_html2pdf(self) -> bool:
655
return ProjectFeature.HTML2PDF in self.project_features
656
657
def is_activated_diff(self) -> bool:
658
return ProjectFeature.DIFF in self.project_features
659
660
def is_activated_reqif(self) -> bool:
661
return ProjectFeature.REQIF in self.project_features
662
663
def is_activated_mathjax(self) -> bool:
664
return ProjectFeature.MATHJAX in self.project_features
665
666
def is_activated_mermaid(self) -> bool:
667
return ProjectFeature.MERMAID in self.project_features
668
669
def is_activated_rapidoc(self) -> bool:
670
return ProjectFeature.RAPIDOC in self.project_features
671
672
def get_project_root_path(self) -> str:
673
if self.input_paths is not None and len(self.input_paths) > 0:
674
return self.input_paths[0]
675
raise NotImplementedError
676
677
def get_strictdoc_root_path(self) -> str:
678
return self.environment.path_to_strictdoc
679
680
def get_path_to_cache_dir(self) -> str:
681
return self.dir_for_sdoc_cache
682
683
def get_static_files_path(self) -> str:
684
return self.environment.get_static_files_path()
685
686
def get_extra_static_files_path(self) -> str:
687
return self.environment.get_extra_static_files_path()
688
689
def get_project_hash(self) -> str:
690
assert self.input_paths is not None and len(self.input_paths) > 0
691
return get_md5(self.input_paths[0])
692
693
def get_relevant_source_nodes_entry(
694
self, path_to_file: str
695
) -> Optional[SourceNodesEntry]:
696
"""
697
Get relevant source_nodes config item for a given source code file.698
699
Returns data for the first entry from source_nodes that is a parent path of path_to_file.700
If path_to_file is absolute, source node config entries are assumed to be in the source_root_path.701
"""702
703
source_root_path = self.source_root_path
704
assert source_root_path is not None
705
assert os.path.exists(source_root_path), source_root_path
706
707
source_file_path = Path(path_to_file)
708
for sdoc_source_config_entry_ in self.source_nodes:
709
# FIXME: Move the setting of full paths to .finalize() of this config710
# class when it is implemented.711
if sdoc_source_config_entry_.full_path is None:
712
sdoc_source_config_entry_.full_path = Path(
713
source_root_path714
) / Path(sdoc_source_config_entry_.path)
715
716
if source_file_path.is_absolute():
717
if (
718
sdoc_source_config_entry_.full_path
719
in source_file_path.parents
720
):721
return sdoc_source_config_entry_
722
else:
723
if (
724
Path(sdoc_source_config_entry_.path)
725
in source_file_path.parents
726
):727
return sdoc_source_config_entry_
728
729
return None
730
731
732
class ProjectConfigLoader:
733
@classmethod734
def load(
735
cls, input_path: str, output_dir: Optional[str] = None
736
) -> ProjectConfig:
737
assert os.path.exists(input_path), input_path
738
project_config: ProjectConfig = cls.load_from_path_or_get_default(
739
path_to_config=input_path
740
)741
project_config.input_paths = [input_path]
742
if output_dir is not None:
743
project_config.output_dir = output_dir
744
project_config.validate_and_finalize()
745
return project_config
746
747
@classmethod748
def load_using_export_config(
749
cls,
750
export_config: ExportCommandConfig,
751
) -> ProjectConfig:
752
path_to_config = export_config.get_path_to_config()
753
project_config: ProjectConfig = cls.load_from_path_or_get_default(
754
path_to_config=path_to_config
755
)756
project_config.integrate_export_config(export_config)
757
project_config.validate_and_finalize()
758
return project_config
759
760
@classmethod761
def load_using_server_config(
762
cls,
763
server_config: ServerCommandConfig,
764
) -> ProjectConfig:
765
path_to_config = server_config.get_path_to_config()
766
project_config: ProjectConfig = cls.load_from_path_or_get_default(
767
path_to_config=path_to_config
768
)769
project_config.integrate_server_config(server_config)
770
project_config.validate_and_finalize()
771
return project_config
772
773
@classmethod774
def load_using_import_excel_config(
775
cls,
776
_: ImportExcelCommandConfig,
777
) -> ProjectConfig:
778
path_to_config = os.getcwd()
779
project_config: ProjectConfig = cls.load_from_path_or_get_default(
780
path_to_config=path_to_config
781
)782
project_config.input_paths = [path_to_config]
783
project_config.validate_and_finalize()
784
return project_config
785
786
@classmethod787
def load_using_import_reqif_config(
788
cls,
789
_: ImportReqIFCommandConfig,
790
) -> ProjectConfig:
791
path_to_config = os.getcwd()
792
project_config: ProjectConfig = cls.load_from_path_or_get_default(
793
path_to_config=path_to_config
794
)795
project_config.input_paths = [path_to_config]
796
project_config.validate_and_finalize()
797
return project_config
798
799
@classmethod800
def load_using_manage_autouid_config(
801
cls,
802
manage_autouid_config: ManageAutoUIDCommandConfig,
803
) -> ProjectConfig:
804
path_to_config = manage_autouid_config.get_path_to_config()
805
806
project_config: ProjectConfig = cls.load_from_path_or_get_default(
807
path_to_config=path_to_config
808
)809
810
# FIXME: Encapsulate all this in project_config.integrate_manage_autouid_config(),811
# following the example of integrate_export_config().812
project_config.input_paths = [manage_autouid_config.input_path]
813
project_config.source_root_path = str(
814
Path(manage_autouid_config.input_path).resolve()
815
)816
project_config.auto_uid_mode = True
817
project_config.autouuid_include_sections = (
818
manage_autouid_config.include_sections
819
)820
821
# FIXME: Traceability Index is coupled with HTML output.822
project_config.export_output_html_root = "NOT_RELEVANT"
823
824
project_config.validate_and_finalize()
825
826
return project_config
827
828
@staticmethod829
def load_from_path_or_get_default(
830
*,
831
path_to_config: str,
832
) -> ProjectConfig:
833
if not os.path.exists(path_to_config):
834
return ProjectConfig.default_config()
835
if os.path.isdir(path_to_config):
836
path_to_config_dir = path_to_config
837
# Prefer the Python config file when both are present.838
path_to_py_config = os.path.join(
839
path_to_config_dir, "strictdoc_config.py"
840
)841
path_to_toml_config = os.path.join(
842
path_to_config_dir, "strictdoc.toml"
843
)844
845
if os.path.isfile(path_to_py_config):
846
path_to_config = path_to_py_config
847
elif os.path.isfile(path_to_toml_config):
848
path_to_config = path_to_toml_config
849
850
if not os.path.isfile(path_to_config):
851
return ProjectConfig.default_config()
852
853
if path_to_config.endswith("strictdoc_config.py"):
854
return ProjectConfigLoader.load_from_python(
855
config_py_path=path_to_config
856
)857
858
try:
859
config_content = toml.load(path_to_config)
860
except toml.decoder.TomlDecodeError as exception:
861
raise StrictDocException( # noqa: T201
862
f"Could not parse the config file {path_to_config}: "
863
f"{exception}."
864
) from None
865
except Exception as exception: # pragma: no cover
866
raise AssertionError from exception
867
868
DEPRECATION_ENGINE.add_message(
869
"DEPRECATED_CONFIG_TOML",
870
(871
"WARNING: StrictDoc TOML configuration format is deprecated. "872
"Replace the TOML config file with a Python config file.\n\n"
873
"See the migration guide for mode details:\n\n"
874
"https://strictdoc.readthedocs.io/en/stable/?a=SECTION-UG-MIGRATE-CONFIG-2025-Q4"875
),876
)877
878
config_last_update = get_file_modification_time(path_to_config)
879
880
return ProjectConfigLoader._load_from_dictionary(
881
config_dict=config_content,
882
config_last_update=config_last_update,
883
)884
885
@staticmethod886
def load_from_python(*, config_py_path: str) -> ProjectConfig:
887
module = import_from_path(config_py_path)
888
create_config_function = module.create_config
889
assert isinstance(create_config_function, types.FunctionType), type(
890
create_config_function891
)892
project_config = create_config_function()
893
assert isinstance(project_config, ProjectConfig)
894
return project_config
895
896
@staticmethod897
def _load_from_dictionary(
898
*,
899
config_dict: Dict[str, Any],
900
config_last_update: Optional[datetime.datetime],
901
) -> ProjectConfig:
902
project_title = ProjectConfigDefault.DEFAULT_PROJECT_TITLE
903
dir_for_sdoc_assets = ProjectConfigDefault.DEFAULT_DIR_FOR_SDOC_ASSETS
904
dir_for_sdoc_cache = ProjectConfigDefault.DEFAULT_DIR_FOR_SDOC_CACHE
905
project_features = ProjectConfigDefault.DEFAULT_FEATURES
906
server_host = ProjectConfigDefault.DEFAULT_SERVER_HOST
907
server_port = ProjectConfigDefault.DEFAULT_SERVER_PORT
908
include_doc_paths: List[str] = []
909
exclude_doc_paths: List[str] = []
910
source_root_path = None
911
include_source_paths: List[str] = []
912
exclude_source_paths: List[str] = []
913
test_report_root_dict: Dict[str, str] = {}
914
source_nodes: List[SourceNodesEntry] = []
915
html2pdf_strict: bool = False
916
html2pdf_template: Optional[str] = None
917
bundle_document_version = (
918
ProjectConfigDefault.DEFAULT_BUNDLE_DOCUMENT_VERSION
919
)920
bundle_document_date = (
921
ProjectConfigDefault.DEFAULT_BUNDLE_DOCUMENT_COMMIT_DATE
922
)923
924
traceability_matrix_relation_columns: Optional[
925
List[Tuple[str, Optional[str]]]
926
] = None
927
reqif_profile = ReqIFProfile.P01_SDOC
928
reqif_multiline_is_xhtml = False
929
reqif_enable_mid = False
930
reqif_import_markup: Optional[str] = None
931
chromedriver: Optional[str] = None
932
933
section_behavior: str = ProjectConfigDefault.DEFAULT_SECTION_BEHAVIOR
934
statistics_generator: Optional[str] = None
935
936
if "project" in config_dict:
937
project_content = config_dict["project"]
938
project_title = project_content.get("title", project_title)
939
dir_for_sdoc_assets = project_content.get(
940
"html_assets_strictdoc_dir", dir_for_sdoc_assets
941
)942
dir_for_sdoc_cache = project_content.get(
943
"cache_dir", dir_for_sdoc_cache
944
)945
946
project_features = project_content.get("features", project_features)
947
948
statistics_generator = project_content.get(
949
"statistics_generator", statistics_generator
950
)951
952
include_doc_paths = project_content.get(
953
"include_doc_paths", include_doc_paths
954
)955
956
exclude_doc_paths = project_content.get(
957
"exclude_doc_paths", exclude_doc_paths
958
)959
960
source_root_path = project_content.get(
961
"source_root_path", source_root_path
962
)963
964
include_source_paths = project_content.get(
965
"include_source_paths", include_source_paths
966
)967
968
exclude_source_paths = project_content.get(
969
"exclude_source_paths", exclude_source_paths
970
)971
972
html2pdf_strict = project_content.get(
973
"html2pdf_strict", html2pdf_strict
974
)975
976
html2pdf_template = project_content.get(
977
"html2pdf_template", html2pdf_template
978
)979
980
bundle_document_version = project_content.get(
981
"bundle_document_version", bundle_document_version
982
)983
984
bundle_document_date = project_content.get(
985
"bundle_document_date", bundle_document_date
986
)987
988
traceability_matrix_relation_columns_config: Optional[List[str]] = (
989
project_content.get(
990
"traceability_matrix_relation_columns", None
991
)992
)993
if traceability_matrix_relation_columns_config is not None:
994
assert isinstance(
995
traceability_matrix_relation_columns_config, list
996
)997
traceability_matrix_relation_columns = []
998
for (
999
relation_column_string_1000
) in traceability_matrix_relation_columns_config:
1001
relation_tuple = parse_relation_tuple(
1002
relation_column_string_1003
)1004
assert relation_tuple is not None
1005
traceability_matrix_relation_columns.append(relation_tuple)
1006
1007
chromedriver = project_content.get("chromedriver", chromedriver)
1008
1009
if (
1010
test_report_root_dict_ := project_content.get(
1011
"test_report_root_dict", None
1012
)1013
) is not None:
1014
assert isinstance(test_report_root_dict_, list), (
1015
test_report_root_dict1016
)1017
for test_report_root_entry_ in test_report_root_dict_:
1018
assert isinstance(test_report_root_entry_, dict)
1019
test_report_root_dict.update(test_report_root_entry_)
1020
1021
section_behavior = project_content.get(
1022
"section_behavior", section_behavior
1023
)1024
assert section_behavior in ("[SECTION]", "[[SECTION]]")
1025
1026
if "source_nodes" in project_content:
1027
source_nodes_config = project_content["source_nodes"]
1028
assert isinstance(source_nodes_config, list)
1029
for item_ in source_nodes_config:
1030
source_node_path = next(iter(item_))
1031
source_node_item = item_[source_node_path]
1032
source_nodes.append(
1033
SourceNodesEntry(
1034
path=source_node_path,
1035
uid=source_node_item["uid"],
1036
node_type=source_node_item["node_type"],
1037
sdoc_to_source_map=source_node_item["map"]
1038
if "map" in source_node_item
1039
else {},
1040
)1041
)1042
1043
if "server" in config_dict:
1044
server_content = config_dict["server"]
1045
server_host = server_content.get("host", server_host)
1046
server_port = server_content.get("port", server_port)
1047
1048
if "reqif" in config_dict:
1049
reqif_content = config_dict["reqif"]
1050
reqif_multiline_is_xhtml = reqif_content.get(
1051
"multiline_is_xhtml", False
1052
)1053
reqif_enable_mid = reqif_content.get("enable_mid", False)
1054
reqif_import_markup = reqif_content.get("import_markup", None)
1055
1056
return ProjectConfig(
1057
project_title=project_title,
1058
dir_for_sdoc_assets=dir_for_sdoc_assets,
1059
dir_for_sdoc_cache=dir_for_sdoc_cache,
1060
project_features=project_features,
1061
server_host=server_host,
1062
server_port=server_port,
1063
include_doc_paths=include_doc_paths,
1064
exclude_doc_paths=exclude_doc_paths,
1065
source_root_path=source_root_path,
1066
include_source_paths=include_source_paths,
1067
exclude_source_paths=exclude_source_paths,
1068
test_report_root_dict=test_report_root_dict,
1069
source_nodes=source_nodes,
1070
html2pdf_strict=html2pdf_strict,
1071
html2pdf_template=html2pdf_template,
1072
bundle_document_version=bundle_document_version,
1073
bundle_document_date=bundle_document_date,
1074
traceability_matrix_relation_columns=traceability_matrix_relation_columns,
1075
reqif_profile=reqif_profile,
1076
reqif_multiline_is_xhtml=reqif_multiline_is_xhtml,
1077
reqif_enable_mid=reqif_enable_mid,
1078
reqif_import_markup=reqif_import_markup,
1079
chromedriver=chromedriver,
1080
section_behavior=section_behavior,
1081
statistics_generator=statistics_generator,
1082
_config_last_update=config_last_update,
1083
)