Skip to content

Commit c361d8c

Browse files
authored
Merge pull request #2543 from haxtibal/tdmg/source_node_remap
feat(backend/sdoc_source_code): Support remapping custom tags to sdoc fields
2 parents e93c3a5 + 6aabd5e commit c361d8c

File tree

14 files changed

+272
-117
lines changed

14 files changed

+272
-117
lines changed

strictdoc/backend/sdoc_source_code/caching_reader.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from typing import Optional, Union
66

7-
from strictdoc.backend.sdoc.models.grammar_element import GrammarElement
87
from strictdoc.backend.sdoc.pickle_cache import PickleCache
98
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
109
SourceFileTraceabilityInfo,
@@ -29,7 +28,7 @@ class SourceFileTraceabilityCachingReader:
2928
def read_from_file(
3029
path_to_file: str,
3130
project_config: ProjectConfig,
32-
source_node_grammar_element: Optional[GrammarElement],
31+
source_node_tags: Optional[set[str]] = None,
3332
) -> Optional[SourceFileTraceabilityInfo]:
3433
unpickled_content = PickleCache.read_from_cache(
3534
path_to_file, project_config, "source_file"
@@ -42,7 +41,9 @@ def read_from_file(
4241
return unpickled_content
4342

4443
reader = SourceFileTraceabilityCachingReader._get_reader(
45-
path_to_file, project_config, source_node_grammar_element
44+
path_to_file,
45+
project_config,
46+
source_node_tags,
4647
)
4748
try:
4849
traceability_info = reader.read_from_file(path_to_file)
@@ -64,7 +65,7 @@ def read_from_file(
6465
def _get_reader(
6566
path_to_file: str,
6667
project_config: ProjectConfig,
67-
source_node_grammar_element: Optional[GrammarElement],
68+
source_node_tags: Optional[set[str]] = None,
6869
) -> Union[
6970
SourceFileTraceabilityReader,
7071
SourceFileTraceabilityReader_Python,
@@ -82,12 +83,9 @@ def _get_reader(
8283
or path_to_file.endswith(".hpp")
8384
or path_to_file.endswith(".cpp")
8485
):
85-
custom_tags = (
86-
source_node_grammar_element.get_field_titles()
87-
if source_node_grammar_element is not None
88-
else None
86+
return SourceFileTraceabilityReader_C(
87+
custom_tags=source_node_tags
8988
)
90-
return SourceFileTraceabilityReader_C(custom_tags=custom_tags)
9189
if path_to_file.endswith(".robot"):
9290
return SourceFileTraceabilityReader_Robot()
9391
return SourceFileTraceabilityReader()

strictdoc/backend/sdoc_source_code/comment_parser/marker_lexer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class GrammarTemplate(Template):
6767
class MarkerLexer:
6868
@staticmethod
6969
def parse(
70-
source_input: str, custom_tags: Optional[list[str]] = None
70+
source_input: str, custom_tags: Optional[set[str]] = None
7171
) -> ParseTree:
7272
if custom_tags is not None:
7373
grammar_extension = NODE_GRAMMAR_EXTENSION.substitute(

strictdoc/backend/sdoc_source_code/marker_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def parse(
3232
comment_line_start: int,
3333
entity_name: Optional[str] = None,
3434
col_offset: int = 0,
35-
custom_tags: Optional[list[str]] = None,
35+
custom_tags: Optional[set[str]] = None,
3636
) -> SourceNode:
3737
"""
3838
Parse relation markers from source file comments.

strictdoc/backend/sdoc_source_code/models/source_node.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55
from dataclasses import dataclass, field
6-
from typing import Dict, List, Optional, Union
6+
from typing import List, Optional, Union
77

88
from strictdoc.backend.sdoc_source_code.models.function import Function
99
from strictdoc.backend.sdoc_source_code.models.function_range_marker import (
@@ -13,6 +13,7 @@
1313
from strictdoc.backend.sdoc_source_code.models.range_marker import (
1414
RangeMarker,
1515
)
16+
from strictdoc.core.project_config import SourceNodesEntry
1617

1718

1819
@dataclass
@@ -21,5 +22,33 @@ class SourceNode:
2122
markers: List[Union[FunctionRangeMarker, RangeMarker, LineMarker]] = field(
2223
default_factory=list
2324
)
24-
fields: Dict[str, str] = field(default_factory=dict)
25+
fields: dict[str, str] = field(default_factory=dict)
2526
function: Optional[Function] = None
27+
28+
def get_sdoc_field(
29+
self,
30+
field_name: str,
31+
cfg_entry: SourceNodesEntry,
32+
) -> Optional[str]:
33+
if field_name in cfg_entry.sdoc_to_source_map:
34+
field_name = cfg_entry.sdoc_to_source_map[field_name]
35+
return self.fields.get(field_name)
36+
37+
def get_sdoc_fields(self, cfg_entry: SourceNodesEntry) -> dict[str, str]:
38+
"""
39+
Get SDoc representation of all available fields.
40+
41+
Returns a dict equivalent to self.fields unless config option 'sdoc_to_source_map' is set.
42+
"""
43+
fields = {
44+
field_name: self.fields[field_name]
45+
for field_name in self.fields
46+
if field_name not in cfg_entry.sdoc_to_source_map.values()
47+
}
48+
for (
49+
field_name,
50+
mapped_field_name,
51+
) in cfg_entry.sdoc_to_source_map.items():
52+
if mapped_field_name in self.fields:
53+
fields[field_name] = self.fields[mapped_field_name]
54+
return fields

strictdoc/backend/sdoc_source_code/reader_c.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@
4040

4141

4242
class SourceFileTraceabilityReader_C:
43-
def __init__(self, custom_tags: Optional[list[str]] = None) -> None:
44-
self.custom_tags: Optional[list[str]] = custom_tags
43+
def __init__(self, custom_tags: Optional[set[str]] = None) -> None:
44+
self.custom_tags: Optional[set[str]] = custom_tags
4545

4646
def read(
4747
self,

strictdoc/core/file_traceability_index.py

Lines changed: 50 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -648,41 +648,46 @@ def create_folder_section(
648648

649649
for source_node_ in traceability_info_.source_nodes:
650650
assert source_node_.entity_name is not None
651-
source_sdoc_node_uid = f"{document_uid}/{path_to_source_file_}/{source_node_.entity_name}"
652-
653-
source_sdoc_node = traceability_index.get_node_by_uid_weak(
654-
source_sdoc_node_uid
651+
sdoc_node_uid = source_node_.get_sdoc_field(
652+
"UID", relevant_source_node_entry
653+
)
654+
if sdoc_node_uid is None:
655+
sdoc_node_uid = f"{document_uid}/{path_to_source_file_}/{source_node_.entity_name}"
656+
sdoc_node = traceability_index.get_node_by_uid_weak(
657+
sdoc_node_uid
655658
)
656-
if source_sdoc_node is not None:
657-
source_sdoc_node = assert_cast(source_sdoc_node, SDocNode)
659+
660+
if sdoc_node is not None:
661+
sdoc_node = assert_cast(sdoc_node, SDocNode)
658662
self.merge_sdoc_node_with_source_node(
659-
source_sdoc_node,
663+
relevant_source_node_entry,
660664
source_node_,
665+
sdoc_node,
661666
document,
662-
relevant_source_node_entry,
663667
)
664668
else:
665-
source_sdoc_node = self.create_sdoc_node_from_source_node(
669+
sdoc_node = self.create_sdoc_node_from_source_node(
666670
source_node_,
667-
source_sdoc_node_uid,
668-
document,
669671
relevant_source_node_entry,
672+
sdoc_node_uid,
673+
document,
670674
)
671-
current_top_node.section_contents.append(source_sdoc_node)
675+
sdoc_node_uid = assert_cast(sdoc_node.reserved_uid, str)
676+
current_top_node.section_contents.append(sdoc_node)
672677
traceability_index.graph_database.create_link(
673678
link_type=GraphLinkType.UID_TO_NODE,
674-
lhs_node=source_sdoc_node_uid,
675-
rhs_node=source_sdoc_node,
679+
lhs_node=sdoc_node_uid,
680+
rhs_node=sdoc_node,
676681
)
677682

678683
self.connect_source_node_function(
679-
source_node_, source_sdoc_node_uid, traceability_info_
684+
source_node_, sdoc_node_uid, traceability_info_
680685
)
681686
self.connect_sdoc_node_with_file_path(
682-
source_sdoc_node, path_to_source_file_
687+
sdoc_node, path_to_source_file_
683688
)
684689
self.connect_source_node_requirements(
685-
source_node_, source_sdoc_node, traceability_index
690+
source_node_, sdoc_node, traceability_index
686691
)
687692

688693
# Warn if source_node was not matched by any include_source_paths, it indicates misconfiguration
@@ -975,45 +980,37 @@ def connect_source_node_function(
975980
@staticmethod
976981
def create_sdoc_node_from_source_node(
977982
source_node: SourceNode,
978-
uid: str,
983+
source_node_config_entry: SourceNodesEntry,
984+
sdoc_node_uid: str,
979985
parent_document: SDocDocumentIF,
980-
relevant_source_node_entry: SourceNodesEntry,
981986
) -> SDocNode:
982-
source_sdoc_node = SDocNode(
987+
sdoc_node = SDocNode(
983988
parent=parent_document,
984-
node_type=relevant_source_node_entry.node_type,
989+
node_type=source_node_config_entry.node_type,
985990
fields=[],
986991
relations=[],
987992
# It is important that this autogenerated node is marked as such.
988993
autogen=True,
989994
)
990-
source_sdoc_node.ng_document_reference = DocumentReference()
991-
source_sdoc_node.ng_document_reference.set_document(parent_document)
992-
source_sdoc_node.ng_including_document_reference = DocumentReference()
993-
source_sdoc_node.set_field_value(
994-
field_name="UID",
995-
form_field_index=0,
996-
value=uid,
997-
)
998-
source_sdoc_node.set_field_value(
999-
field_name="TITLE",
1000-
form_field_index=0,
1001-
value=source_node.entity_name,
1002-
)
1003-
for node_name_, node_value_ in source_node.fields.items():
1004-
source_sdoc_node.set_field_value(
1005-
field_name=node_name_,
1006-
form_field_index=0,
1007-
value=node_value_,
1008-
)
1009-
return source_sdoc_node
995+
sdoc_node.ng_document_reference = DocumentReference()
996+
sdoc_node.ng_document_reference.set_document(parent_document)
997+
sdoc_node.ng_including_document_reference = DocumentReference()
998+
sdoc_node_fields = source_node.get_sdoc_fields(source_node_config_entry)
999+
sdoc_node_fields["UID"] = sdoc_node_uid
1000+
if (
1001+
"TITLE" not in sdoc_node_fields
1002+
and source_node.entity_name is not None
1003+
):
1004+
sdoc_node_fields["TITLE"] = source_node.entity_name
1005+
FileTraceabilityIndex.set_sdoc_node_fields(sdoc_node, sdoc_node_fields)
1006+
return sdoc_node
10101007

10111008
@staticmethod
10121009
def merge_sdoc_node_with_source_node(
1013-
sdoc_node: SDocNode,
1010+
source_node_config_entry: SourceNodesEntry,
10141011
source_node: SourceNode,
1012+
sdoc_node: SDocNode,
10151013
parent_document: SDocDocumentIF,
1016-
source_node_config_entry: SourceNodesEntry,
10171014
) -> None:
10181015
# First check if grammar element definitions are compatible.
10191016
source_node_type = source_node_config_entry.node_type
@@ -1038,19 +1035,19 @@ def merge_sdoc_node_with_source_node(
10381035
f"Grammar element {sdoc_node_document.reserved_uid}::{source_node_type} "
10391036
f"incompatible with {parent_document.reserved_uid}::{source_node_type}"
10401037
)
1041-
# Merge strategy: overwrite title if there's a TITLE from custom tags.
1042-
if "TITLE" in source_node.fields:
1043-
sdoc_node.set_field_value(
1044-
field_name="TITLE",
1045-
form_field_index=0,
1046-
value=source_node.fields["TITLE"],
1047-
)
10481038
# Merge strategy: overwrite any field if there's a field with same name from custom tags.
1049-
for node_name_, node_value_ in source_node.fields.items():
1039+
sdoc_node_fields = source_node.get_sdoc_fields(source_node_config_entry)
1040+
FileTraceabilityIndex.set_sdoc_node_fields(sdoc_node, sdoc_node_fields)
1041+
1042+
@staticmethod
1043+
def set_sdoc_node_fields(
1044+
sdoc_node: SDocNode, sdoc_node_fields: dict[str, str]
1045+
) -> None:
1046+
for field_name, field_value in sdoc_node_fields.items():
10501047
sdoc_node.set_field_value(
1051-
field_name=node_name_,
1048+
field_name=field_name,
10521049
form_field_index=0,
1053-
value=node_value_,
1050+
value=field_value,
10541051
)
10551052

10561053
def connect_sdoc_node_with_file_path(

strictdoc/core/project_config.py

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import sys
99
import tempfile
1010
import types
11-
from dataclasses import dataclass
11+
from dataclasses import dataclass, field
1212
from enum import Enum
1313
from pathlib import Path
1414
from typing import Any, Dict, List, Optional, Tuple
@@ -47,12 +47,8 @@ class SourceNodesEntry:
4747
path: str
4848
uid: str
4949
node_type: str
50-
full_path: Optional[Path]
51-
52-
@classmethod
53-
def from_cfg_data(cls, data: dict[str, str]) -> "SourceNodesEntry":
54-
full_path = Path(data["full_path"]) if "full_path" in data else None
55-
return cls(data["path"], data["uid"], data["node_type"], full_path)
50+
sdoc_to_source_map: Dict[str, str] = field(default_factory=dict)
51+
full_path: Optional[Path] = None
5652

5753

5854
class ProjectFeature(str, Enum):
@@ -124,7 +120,7 @@ def __init__(
124120
include_source_paths: Optional[List[str]] = None,
125121
exclude_source_paths: Optional[List[str]] = None,
126122
test_report_root_dict: Optional[Dict[str, str]] = None,
127-
source_nodes: Optional[List[Dict[str, str]]] = None,
123+
source_nodes: Optional[List[SourceNodesEntry]] = None,
128124
html2pdf_strict: bool = False,
129125
html2pdf_template: Optional[str] = None,
130126
bundle_document_version: Optional[
@@ -209,12 +205,7 @@ def __init__(
209205
test_report_root_dict if test_report_root_dict is not None else {}
210206
)
211207
self.source_nodes: List[SourceNodesEntry] = (
212-
[
213-
SourceNodesEntry.from_cfg_data(source_node)
214-
for source_node in source_nodes
215-
]
216-
if source_nodes is not None
217-
else []
208+
source_nodes if source_nodes is not None else []
218209
)
219210

220211
# Settings derived from the command-line parameters.
@@ -585,7 +576,7 @@ def _load_from_dictionary(
585576
include_source_paths: List[str] = []
586577
exclude_source_paths: List[str] = []
587578
test_report_root_dict: Dict[str, str] = {}
588-
source_nodes: List[Dict[str, str]] = []
579+
source_nodes: List[SourceNodesEntry] = []
589580
html2pdf_strict: bool = False
590581
html2pdf_template: Optional[str] = None
591582
bundle_document_version = (
@@ -801,14 +792,16 @@ def _load_from_dictionary(
801792
assert isinstance(source_nodes_config, list)
802793
for item_ in source_nodes_config:
803794
source_node_path = next(iter(item_))
804-
source_node_uid = item_[source_node_path]["uid"]
805-
source_node_node_type = item_[source_node_path]["node_type"]
795+
source_node_item = item_[source_node_path]
806796
source_nodes.append(
807-
{
808-
"path": source_node_path,
809-
"uid": source_node_uid,
810-
"node_type": source_node_node_type,
811-
}
797+
SourceNodesEntry(
798+
path=source_node_path,
799+
uid=source_node_item["uid"],
800+
node_type=source_node_item["node_type"],
801+
sdoc_to_source_map=source_node_item["map"]
802+
if "map" in source_node_item
803+
else {},
804+
)
812805
)
813806

814807
if "server" in config_dict:

strictdoc/core/traceability_index.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from strictdoc.backend.sdoc.models.anchor import Anchor
1111
from strictdoc.backend.sdoc.models.document import SDocDocument
1212
from strictdoc.backend.sdoc.models.document_config import DocumentConfig
13+
from strictdoc.backend.sdoc.models.grammar_element import GrammarElement
1314
from strictdoc.backend.sdoc.models.inline_link import InlineLink
1415
from strictdoc.backend.sdoc.models.node import SDocNode
1516
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
@@ -353,6 +354,14 @@ def get_incoming_links(
353354
# FIXME: Should the graph database return OrderedSet or a copied list()?
354355
return list(incoming_links)
355356

357+
def get_grammar_element(
358+
self, document_uid: str, node_type: str
359+
) -> Optional[GrammarElement]:
360+
document = self.get_node_by_uid_weak2(document_uid)
361+
if isinstance(document, SDocDocument) and document.grammar is not None:
362+
return document.grammar.elements_by_type.get(node_type)
363+
return None
364+
356365
def create_traceability_info(
357366
self,
358367
source_file: SourceFile,

0 commit comments

Comments
 (0)