Path:
strictdoc/core/traceability_index.py
Lines:
1435
Non-empty lines:
1264
Non-empty lines covered with requirements:
1264 / 1264 (100.0%)
Functions:
69
Functions covered by requirements:
69 / 69 (100.0%)
1
"""2
@relation(SDOC-SRS-28, scope=file)3
"""4
5
import datetime
6
from copy import copy, deepcopy
7
from typing import Any, Dict, Generator, List, Optional, Tuple, Union
8
9
from strictdoc.backend.sdoc.document_reference import DocumentReference
10
from strictdoc.backend.sdoc.models.anchor import Anchor
11
from strictdoc.backend.sdoc.models.document import SDocDocument
12
from strictdoc.backend.sdoc.models.document_config import DocumentConfig
13
from strictdoc.backend.sdoc.models.grammar_element import GrammarElement
14
from strictdoc.backend.sdoc.models.inline_link import InlineLink
15
from strictdoc.backend.sdoc.models.node import SDocNode
16
from strictdoc.backend.sdoc.models.reference import (
17
ChildReqReference,
18
ParentReqReference,
19
)20
from strictdoc.backend.sdoc.node_filter import NodeFilter
21
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
22
RelationMarkerType,
23
SourceFileTraceabilityInfo,
24
)25
from strictdoc.core.asset_manager import AssetManager
26
from strictdoc.core.constants import GraphLinkType
27
from strictdoc.core.document_iterator import SDocDocumentIterator
28
from strictdoc.core.document_meta import DocumentMeta
29
from strictdoc.core.document_tree import DocumentTree
30
from strictdoc.core.file_dependency_manager import FileDependencyManager
31
from strictdoc.core.file_system.source_tree import SourceFile
32
from strictdoc.core.file_traceability_index import FileTraceabilityIndex
33
from strictdoc.core.graph.abstract_bucket import ALL_EDGES
34
from strictdoc.core.graph_database import GraphDatabase
35
from strictdoc.core.project_config import ProjectConfig
36
from strictdoc.core.transforms.constants import NodeCreationOrder
37
from strictdoc.core.transforms.validation_error import (
38
MultipleValidationErrorAsList,
39
SingleValidationError,
40
)41
from strictdoc.core.tree_cycle_detector import TreeCycleDetector
42
from strictdoc.core.validation_index import ValidationIndex
43
from strictdoc.helpers.cast import assert_cast, assert_optional_cast
44
from strictdoc.helpers.file_modification_time import set_file_modification_time
45
from strictdoc.helpers.mid import MID
46
from strictdoc.helpers.ordered_set import OrderedSet
47
from strictdoc.helpers.paths import SDocRelativePath
48
from strictdoc.helpers.sorting import alphanumeric_sort
49
50
51
class TraceabilityIndex:
52
def __init__(
53
self,
54
document_tree: DocumentTree,
55
document_iterators: Dict[SDocDocument, SDocDocumentIterator],
56
file_traceability_index: FileTraceabilityIndex,
57
graph_database: GraphDatabase,
58
file_dependency_manager: FileDependencyManager,
59
):60
self._document_iterators: Dict[SDocDocument, SDocDocumentIterator] = (
61
document_iterators62
)63
self._file_traceability_index: FileTraceabilityIndex = (
64
file_traceability_index65
)66
self.validation_index: ValidationIndex = ValidationIndex()
67
self.graph_database: GraphDatabase = graph_database
68
self.document_tree: DocumentTree = document_tree
69
self.asset_manager: Optional[AssetManager] = None
70
self.file_dependency_manager: FileDependencyManager = (
71
file_dependency_manager72
)73
self.node_filter: Optional[NodeFilter] = None
74
self.pending_inline_links: List[InlineLink] = []
75
76
self.index_last_updated = datetime.datetime.today()
77
self.contains_included_documents = False
78
self.strictdoc_last_update: datetime.datetime = (
79
datetime.datetime.fromtimestamp(0)
80
)81
82
# The timestamp is used by HTML/JS for invalidating the search index83
# cache in the IndexedDB database.84
# If no documents have to be re-generated with the second+ run of85
# StrictDoc, this timestamp is set of a modification date of the first86
# SDoc document, see export_static_html_search_index.87
self.search_index_timestamp: datetime.datetime = datetime.datetime.now(
88
datetime.timezone.utc
89
)90
91
@property92
def document_iterators(self) -> Dict[SDocDocument, SDocDocumentIterator]:
93
return self._document_iterators
94
95
def is_small_project(self) -> bool:
96
"""
97
Check if project is small to control feature availability.98
99
This method helps to decide if StrictDoc will precompile Jinja templates100
to Python files or not. Precompilation may take half a second time, so101
it is only worth doing it when a project is relatively large.102
103
Below, making some assumptions about what makes a small or larger104
project.105
"""106
if len(self.document_tree.document_list) >= 3:
107
return False
108
for document_ in self.document_tree.document_list:
109
if len(document_.section_contents) > 5:
110
return False
111
return (
112
self.graph_database.get_count(
113
link_type=GraphLinkType.NODE_TO_PARENT_NODES
114
)115
<= 10
116
)117
118
def can_edit_document(self, document: SDocDocument) -> bool:
119
assert isinstance(document, SDocDocument), document
120
return not document.autogen
121
122
def can_edit_node(self, node: Union[SDocDocument, SDocNode]) -> bool:
123
assert isinstance(node, (SDocDocument, SDocNode)), node
124
if isinstance(node, SDocDocument):
125
return not node.autogen
126
127
if node.get_parent_or_including_document().autogen or node.autogen:
128
return False
129
130
return True
131
132
def can_delete_node(self, node: Union[SDocDocument, SDocNode]) -> bool:
133
assert isinstance(node, (SDocDocument, SDocNode)), node
134
if isinstance(node, SDocDocument):
135
return not node.autogen
136
137
if node.get_parent_or_including_document().autogen:
138
return False
139
140
return not node.is_managed_by_source_code()
141
142
def can_clone_node(self, node: Union[SDocDocument, SDocNode]) -> bool:
143
assert isinstance(node, (SDocDocument, SDocNode)), node
144
if isinstance(node, SDocDocument):
145
return False
146
147
return self.can_delete_node(node)
148
149
def can_add_node(self, node: Union[SDocDocument, SDocNode]) -> bool:
150
assert isinstance(node, (SDocDocument, SDocNode)), node
151
if isinstance(node, SDocDocument):
152
return not node.autogen
153
154
if node.get_parent_or_including_document().autogen or node.autogen:
155
return False
156
157
return True
158
159
def can_insert_next_to_node(
160
self, node: Union[SDocDocument, SDocNode]
161
) -> bool:
162
assert isinstance(node, (SDocDocument, SDocNode)), node
163
if isinstance(node, SDocDocument):
164
return True
165
166
if node.get_parent_or_including_document().autogen:
167
return False
168
169
parent = node.parent
170
if isinstance(parent, SDocNode) and parent.autogen:
171
return False
172
173
return True
174
175
def can_create_node_at(
176
self, reference_node: Union[SDocDocument, SDocNode], whereto: str
177
) -> bool:
178
assert isinstance(reference_node, (SDocDocument, SDocNode)), (
179
reference_node180
)181
assert isinstance(whereto, str), whereto
182
183
if whereto == NodeCreationOrder.CHILD:
184
return self.can_add_node(reference_node)
185
if whereto in (NodeCreationOrder.BEFORE, NodeCreationOrder.AFTER):
186
return self.can_insert_next_to_node(reference_node)
187
return False
188
189
def can_move_node(self, node: Union[SDocDocument, SDocNode]) -> bool:
190
assert isinstance(node, (SDocDocument, SDocNode)), node
191
if isinstance(node, SDocDocument):
192
return not node.autogen
193
194
if node.get_parent_or_including_document().autogen or node.autogen:
195
return False
196
197
return True
198
199
def can_move_node_to(
200
self,
201
moved_node: SDocNode,
202
target_node: Union[SDocDocument, SDocNode],
203
whereto: str,
204
) -> bool:
205
assert isinstance(moved_node, SDocNode), moved_node
206
assert isinstance(target_node, (SDocDocument, SDocNode)), target_node
207
assert isinstance(whereto, str), whereto
208
209
if not self.can_move_node(moved_node):
210
return False
211
212
if (
213
whereto == NodeCreationOrder.CHILD
214
and isinstance(target_node, SDocNode)
215
and not target_node.is_composite
216
):217
return self.can_insert_next_to_node(target_node)
218
219
return self.can_create_node_at(target_node, whereto)
220
221
def has_parent_requirements(self, requirement: SDocNode) -> bool:
222
assert isinstance(requirement, SDocNode)
223
if not isinstance(requirement.reserved_uid, str):
224
return False
225
226
parent_requirements = self.graph_database.get_link_values(
227
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
228
lhs_node=requirement,
229
edge=ALL_EDGES,
230
)231
return len(parent_requirements) > 0
232
233
def has_children_requirements(self, requirement: SDocNode) -> bool:
234
assert isinstance(requirement, SDocNode)
235
if not isinstance(requirement.reserved_uid, str):
236
return False
237
238
children_requirements = self.graph_database.get_link_values(
239
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
240
lhs_node=requirement,
241
edge=ALL_EDGES,
242
)243
return len(children_requirements) > 0
244
245
def has_source_file_reqs(self, source_file_rel_path: str) -> bool:
246
return self._file_traceability_index.has_source_file_reqs(
247
source_file_rel_path248
)249
250
def has_node_connections(self, node_uid: str) -> bool:
251
assert isinstance(node_uid, str), node_uid
252
return self.graph_database.has_any_link(
253
link_type=GraphLinkType.UID_TO_NODE,
254
lhs_node=node_uid,
255
)256
257
def get_node_by_mid(self, node_mid: MID) -> Any:
258
assert isinstance(node_mid, MID), node_mid
259
return self.graph_database.get_link_value(
260
link_type=GraphLinkType.MID_TO_NODE, lhs_node=node_mid
261
)262
263
def get_node_by_mid_weak(self, node_mid: MID) -> Optional[Any]:
264
assert isinstance(node_mid, MID), node_mid
265
return self.graph_database.get_link_value_weak(
266
link_type=GraphLinkType.MID_TO_NODE, lhs_node=node_mid
267
)268
269
def get_file_traceability_index(self) -> FileTraceabilityIndex:
270
return self._file_traceability_index
271
272
def get_document_by_title(
273
self,
274
document_title: str,
275
) -> SDocDocument:
276
for document_ in self.document_tree.document_list:
277
if document_.reserved_title == document_title:
278
return document_
279
raise LookupError(f"Document not found: '{document_title}'")
280
281
def get_document_iterator(
282
self, document: SDocDocument
283
) -> SDocDocumentIterator:
284
return SDocDocumentIterator(
285
document=document, node_filter=self.node_filter
286
)287
288
def get_parent_requirements(self, requirement: SDocNode) -> List[SDocNode]:
289
assert isinstance(requirement, SDocNode)
290
if not isinstance(requirement.reserved_uid, str):
291
return []
292
293
return list(
294
self.graph_database.get_link_values(
295
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
296
lhs_node=requirement,
297
edge=ALL_EDGES,
298
)299
)300
301
def get_parent_relations_with_roles(
302
self, node: SDocNode
303
) -> List[Tuple[SDocNode, Optional[str]]]:
304
assert isinstance(node, SDocNode)
305
306
return list(
307
self.graph_database.get_link_values_with_edges(
308
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
309
lhs_node=node,
310
edge=ALL_EDGES,
311
)312
)313
314
def get_parent_relations_with_role(
315
self, requirement: SDocNode, role: Optional[str]
316
) -> List[Tuple[SDocNode, Optional[str]]]:
317
assert isinstance(requirement, SDocNode)
318
if requirement.reserved_uid is None:
319
return []
320
321
return list(
322
self.graph_database.get_link_values_with_edges(
323
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
324
lhs_node=requirement,
325
edge=role,
326
)327
)328
329
def get_child_relations_with_roles(
330
self, requirement: SDocNode
331
) -> List[Tuple[SDocNode, Optional[str]]]:
332
assert isinstance(requirement, SDocNode)
333
334
return list(
335
self.graph_database.get_link_values_with_edges(
336
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
337
lhs_node=requirement,
338
edge=ALL_EDGES,
339
)340
)341
342
def get_child_relations_with_role(
343
self, requirement: SDocNode, role: Optional[str]
344
) -> List[Tuple[SDocNode, Optional[str]]]:
345
assert isinstance(requirement, SDocNode)
346
if requirement.reserved_uid is None:
347
return []
348
349
return list(
350
self.graph_database.get_link_values_with_edges(
351
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
352
lhs_node=requirement,
353
edge=role,
354
)355
)356
357
def get_display_role_for_parent_relation(
358
self,
359
node: SDocNode,
360
parent_node: SDocNode,
361
relation_role: Optional[str],
362
) -> Optional[str]:
363
return self._get_display_role_for_relation(
364
node=node,
365
related_node=parent_node,
366
forward_relation_type="Parent",
367
reverse_relation_type="Child",
368
relation_role=relation_role,
369
)370
371
def get_display_role_for_child_relation(
372
self,
373
node: SDocNode,
374
child_node: SDocNode,
375
relation_role: Optional[str],
376
) -> Optional[str]:
377
return self._get_display_role_for_relation(
378
node=node,
379
related_node=child_node,
380
forward_relation_type="Child",
381
reverse_relation_type="Parent",
382
relation_role=relation_role,
383
)384
385
def _get_display_role_for_relation(
386
self,
387
node: SDocNode,
388
related_node: SDocNode,
389
forward_relation_type: str,
390
reverse_relation_type: str,
391
relation_role: Optional[str],
392
) -> Optional[str]:
393
if self._node_has_direct_relation(
394
source_node=node,
395
target_node=related_node,
396
relation_type=forward_relation_type,
397
relation_role=relation_role,
398
):399
return relation_role
400
401
if self._node_has_direct_relation(
402
source_node=related_node,
403
target_node=node,
404
relation_type=reverse_relation_type,
405
relation_role=relation_role,
406
):407
reverse_role = self._get_reverse_role_from_node_grammar(
408
node=related_node,
409
relation_type=reverse_relation_type,
410
relation_role=relation_role,
411
)412
return reverse_role if reverse_role is not None else relation_role
413
414
return relation_role
415
416
@staticmethod417
def _node_has_direct_relation(
418
source_node: SDocNode,
419
target_node: SDocNode,
420
relation_type: str,
421
relation_role: Optional[str],
422
) -> bool:
423
for relation_ in source_node.relations:
424
if not isinstance(
425
relation_, (ParentReqReference, ChildReqReference)
426
):427
continue428
if (
429
relation_.ref_type == relation_type
430
and relation_.ref_uid == target_node.reserved_uid
431
and relation_.role == relation_role
432
):433
return True
434
return False
435
436
@staticmethod437
def _get_reverse_role_from_node_grammar(
438
node: SDocNode,
439
relation_type: str,
440
relation_role: Optional[str],
441
) -> Optional[str]:
442
document = assert_optional_cast(node.get_document(), SDocDocument)
443
if document is None or document.grammar is None:
444
return None
445
446
grammar_element = document.grammar.elements_by_type.get(node.node_type)
447
if grammar_element is None:
448
return None
449
450
return grammar_element.get_relation_reverse_role(
451
relation_type=relation_type,
452
relation_role=relation_role,
453
)454
455
def get_children_requirements(
456
self, requirement: SDocNode
457
) -> List[SDocNode]:
458
assert isinstance(requirement, SDocNode)
459
if not isinstance(requirement.reserved_uid, str):
460
return []
461
462
return list(
463
self.graph_database.get_link_values(
464
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
465
lhs_node=requirement,
466
edge=ALL_EDGES,
467
)468
)469
470
def has_tags(self, document: SDocDocument) -> bool:
471
return self.graph_database.has_any_link(
472
link_type=GraphLinkType.DOCUMENT_TO_TAGS,
473
lhs_node=document.reserved_mid,
474
)475
476
def get_counted_tags(
477
self, document: SDocDocument
478
) -> Generator[Tuple[str, int], None, None]:
479
document_tags_or_none = self.graph_database.get_link_value(
480
link_type=GraphLinkType.DOCUMENT_TO_TAGS,
481
lhs_node=document.reserved_mid,
482
)483
document_tags: Dict[str, int] = assert_cast(document_tags_or_none, dict)
484
485
tags = sorted(document_tags.keys(), key=alphanumeric_sort)
486
for tag in tags:
487
yield tag, document_tags[tag]
488
489
def get_requirement_file_links(
490
self, requirement: SDocNode
491
) -> List[Tuple[str, List[RelationMarkerType]]]:
492
return self._file_traceability_index.get_requirement_file_links(
493
requirement494
)495
496
def get_source_file_reqs(
497
self, source_file_rel_path: str
498
) -> Optional[List[SDocNode]]:
499
return self._file_traceability_index.get_source_file_reqs(
500
source_file_rel_path501
)502
503
def get_coverage_info(
504
self, source_file_rel_path: str
505
) -> SourceFileTraceabilityInfo:
506
return self._file_traceability_index.get_coverage_info(
507
source_file_rel_path508
)509
510
def get_coverage_info_weak(
511
self, source_file_rel_path: str
512
) -> Optional[SourceFileTraceabilityInfo]:
513
return self._file_traceability_index.get_coverage_info_weak(
514
source_file_rel_path515
)516
517
def get_node_by_uid(self, uid: str) -> Any:
518
assert isinstance(uid, str) and len(uid) > 0, uid
519
return self.graph_database.get_link_value(
520
link_type=GraphLinkType.UID_TO_NODE, lhs_node=uid
521
)522
523
def get_node_by_uid_weak2(self, uid: str) -> Optional[Any]:
524
"""
525
FIXME: This can likely replace _weak below with no problem.526
"""527
assert isinstance(uid, str) and len(uid) > 0, uid
528
return self.graph_database.get_link_value_weak(
529
link_type=GraphLinkType.UID_TO_NODE, lhs_node=uid
530
)531
532
def get_linkable_node_by_uid(
533
self, uid: str
534
) -> Union[SDocDocument, SDocNode, Anchor]:
535
return assert_cast(
536
self.get_node_by_uid(uid),
537
(SDocDocument, SDocNode, Anchor),
538
)539
540
def get_node_by_uid_weak(
541
self, uid: str
542
) -> Union[SDocDocument, SDocNode, None]:
543
assert isinstance(uid, str), uid
544
for document in self.document_tree.document_list:
545
document_iterator = SDocDocumentIterator(document)
546
for node, _ in document_iterator.all_content(print_fragments=False):
547
if isinstance(node, SDocDocument):
548
if node.config.uid == uid:
549
return node
550
elif isinstance(node, SDocNode):
551
if node.reserved_uid == uid:
552
return node
553
else:
554
raise NotImplementedError
555
return None
556
557
def get_linkable_node_by_uid_weak(
558
self, uid: str
559
) -> Union[SDocDocument, SDocNode, Anchor, None]:
560
return assert_optional_cast(
561
self.graph_database.get_link_value_weak(
562
link_type=GraphLinkType.UID_TO_NODE, lhs_node=uid
563
),564
(SDocDocument, SDocNode, Anchor),
565
)566
567
def get_incoming_links(
568
self, node: Union[SDocDocument, SDocNode, Anchor]
569
) -> Optional[List[InlineLink]]:
570
incoming_links = self.graph_database.get_link_values(
571
link_type=GraphLinkType.NODE_TO_INCOMING_LINKS,
572
lhs_node=node.reserved_mid,
573
)574
if incoming_links is None or len(incoming_links) == 0:
575
return None
576
# FIXME: Should the graph database return OrderedSet or a copied list()?577
return list(incoming_links)
578
579
def get_grammar_element(
580
self, document_uid: str, node_type: str
581
) -> Optional[GrammarElement]:
582
document = self.get_node_by_uid_weak2(document_uid)
583
if isinstance(document, SDocDocument) and document.grammar is not None:
584
return document.grammar.elements_by_type.get(node_type)
585
return None
586
587
def create_traceability_info(
588
self,
589
source_file: SourceFile,
590
traceability_info: SourceFileTraceabilityInfo,
591
) -> None:
592
self._file_traceability_index.create_traceability_info(
593
source_file, traceability_info
594
)595
596
def create_document(self, document: SDocDocument) -> None:
597
assert isinstance(document, SDocDocument)
598
if document.reserved_uid is not None:
599
self.graph_database.create_link(
600
link_type=GraphLinkType.UID_TO_NODE,
601
lhs_node=document.reserved_uid,
602
rhs_node=document,
603
)604
self.graph_database.create_link(
605
link_type=GraphLinkType.MID_TO_NODE,
606
lhs_node=document.reserved_mid,
607
rhs_node=document,
608
)609
610
def create_inline_link(self, new_link: InlineLink) -> None:
611
assert isinstance(new_link, InlineLink)
612
613
# InlineLink points to a section, node or to anchor.614
assert self.graph_database.has_any_link(
615
link_type=GraphLinkType.UID_TO_NODE, lhs_node=new_link.link
616
)617
618
node_or_anchor: Union[SDocDocument, SDocNode, Anchor] = assert_cast(
619
self.graph_database.get_link_value(
620
link_type=GraphLinkType.UID_TO_NODE,
621
lhs_node=new_link.link,
622
),623
(SDocDocument, SDocNode, Anchor),
624
)625
self.graph_database.create_link(
626
link_type=GraphLinkType.NODE_TO_INCOMING_LINKS,
627
lhs_node=node_or_anchor.reserved_mid,
628
rhs_node=new_link,
629
)630
self.graph_database.create_link(
631
link_type=GraphLinkType.MID_TO_NODE,
632
lhs_node=new_link.reserved_mid,
633
rhs_node=new_link,
634
)635
636
def update_last_updated(self) -> None:
637
"""
638
Update the index's last updated date to the current time.639
640
This is a rather broad way of signalling that all documents of the index641
need to be re-generated when they are opened next time. Several UI642
actions use this method to ensure a complete re-generation of all643
documents.644
"""645
self.index_last_updated = datetime.datetime.today()
646
647
def create_requirement(self, requirement: SDocNode) -> None:
648
assert isinstance(requirement, SDocNode), requirement
649
650
self.graph_database.create_link(
651
link_type=GraphLinkType.MID_TO_NODE,
652
lhs_node=requirement.reserved_mid,
653
rhs_node=requirement,
654
)655
if requirement.reserved_uid is not None:
656
self.graph_database.create_link(
657
link_type=GraphLinkType.UID_TO_NODE,
658
lhs_node=requirement.reserved_uid,
659
rhs_node=requirement,
660
)661
662
def update_requirement_uid(
663
self, requirement: SDocNode, old_uid: Optional[str]
664
) -> None:
665
if old_uid is None:
666
if requirement.reserved_uid:
667
self.graph_database.create_link(
668
link_type=GraphLinkType.UID_TO_NODE,
669
lhs_node=requirement.reserved_uid,
670
rhs_node=requirement,
671
)672
return673
674
self.graph_database.delete_link(
675
link_type=GraphLinkType.UID_TO_NODE,
676
lhs_node=old_uid,
677
rhs_node=requirement,
678
)679
680
if requirement.reserved_uid is not None:
681
self.graph_database.create_link(
682
link_type=GraphLinkType.UID_TO_NODE,
683
lhs_node=requirement.reserved_uid,
684
rhs_node=requirement,
685
)686
687
def update_requirement_parent_uid(
688
self, requirement: SDocNode, parent_uid: str, role: Optional[str]
689
) -> None:
690
assert requirement.reserved_uid is not None
691
assert isinstance(parent_uid, str), parent_uid
692
assert role is None or len(role) > 0, role
693
parent_nodes: OrderedSet[SDocNode] = (
694
self.graph_database.get_link_values(
695
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
696
lhs_node=requirement,
697
edge=role,
698
)699
)700
# If a relation to the parent uid through a given role already exists,701
# there is nothing to do.702
if any(node_.reserved_uid == parent_uid for node_ in parent_nodes):
703
return704
705
parent_requirement: SDocNode = self.graph_database.get_link_value(
706
link_type=GraphLinkType.UID_TO_NODE,
707
lhs_node=parent_uid,
708
)709
710
document = assert_cast(requirement.get_document(), SDocDocument)
711
parent_requirement_document = assert_cast(
712
parent_requirement.get_document(), SDocDocument
713
)714
715
self.graph_database.create_link(
716
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
717
lhs_node=requirement,
718
rhs_node=parent_requirement,
719
edge=role,
720
)721
self.graph_database.create_link(
722
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
723
lhs_node=parent_requirement,
724
rhs_node=requirement,
725
edge=role,
726
)727
728
cycle_detector = TreeCycleDetector()
729
730
def parent_cycle_traverse_(node_id: str) -> List[Any]:
731
node = self.graph_database.get_link_value(
732
link_type=GraphLinkType.UID_TO_NODE,
733
lhs_node=node_id,
734
)735
return list(
736
map(
737
lambda node_: node_.reserved_uid,
738
self.graph_database.get_link_values(
739
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
740
lhs_node=node,
741
),742
)743
)744
745
cycle_detector.check_node(
746
requirement.reserved_uid,
747
parent_cycle_traverse_,
748
)749
750
# Mark document and parent document (if different) for re-generation.751
assert document.meta is not None
752
set_file_modification_time(
753
document.meta.input_doc_full_path, datetime.datetime.today()
754
)755
if parent_requirement_document != document:
756
assert parent_requirement_document.meta is not None
757
set_file_modification_time(
758
parent_requirement_document.meta.input_doc_full_path,
759
datetime.datetime.today(),
760
)761
762
def update_node_mid(self, node: SDocNode, new_mid: str) -> None:
763
"""
764
Update a node’s MID identifier and update all parent and child node765
relations to reference this new MID.766
767
The graph database contains relations between nodes. Since these768
relations remain unchanged, the job of this function is only to update769
each node's original .relations, so that they point to the new MID.770
"""771
772
old_mid = node.reserved_mid
773
774
#775
# Update all child nodes.776
#777
child_nodes: OrderedSet[SDocNode] = self.graph_database.get_link_values(
778
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
779
lhs_node=node,
780
edge=ALL_EDGES,
781
)782
for child_node_ in child_nodes:
783
child_node_document = child_node_.get_document()
784
assert child_node_document is not None
785
786
if child_node_document.config.relation_field != "MID":
787
continue788
789
for relation_ in copy(child_node_.relations):
790
if (
791
isinstance(relation_, ParentReqReference)
792
and relation_.ref_uid == old_mid
793
):794
relation_.ref_uid = new_mid
795
796
#797
# Update all parent nodes.798
#799
parent_nodes: OrderedSet[SDocNode] = (
800
self.graph_database.get_link_values(
801
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
802
lhs_node=node,
803
edge=ALL_EDGES,
804
)805
)806
for parent_node_ in parent_nodes:
807
parent_node_document = parent_node_.get_document()
808
assert parent_node_document is not None
809
810
if parent_node_document.config.relation_field != "MID":
811
continue812
813
for relation_ in copy(parent_node_.relations):
814
if (
815
isinstance(relation_, ChildReqReference)
816
and relation_.ref_uid == old_mid
817
):818
relation_.ref_uid = new_mid
819
820
#821
# Update the node itself.822
#823
node.set_field_value(
824
field_name="MID",
825
form_field_index=0,
826
value=new_mid,
827
)828
829
def update_requirement_child_uid(
830
self, requirement: SDocNode, child_uid: str, role: Optional[str]
831
) -> None:
832
assert requirement.reserved_uid is not None
833
assert isinstance(child_uid, str), child_uid
834
assert role is None or len(role) > 0, role
835
child_nodes: OrderedSet[SDocNode] = self.graph_database.get_link_values(
836
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
837
lhs_node=requirement,
838
edge=role,
839
)840
841
# If a relation to the parent uid through a given role already exists,842
# there is nothing to do.843
if any(node_.reserved_uid == child_uid for node_ in child_nodes):
844
return845
child_requirement = self.graph_database.get_link_value(
846
link_type=GraphLinkType.UID_TO_NODE,
847
lhs_node=child_uid,
848
)849
850
document = assert_cast(requirement.get_document(), SDocDocument)
851
child_requirement_document = assert_cast(
852
child_requirement.get_document(), SDocDocument
853
)854
855
self.graph_database.create_link(
856
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
857
lhs_node=child_requirement,
858
rhs_node=requirement,
859
edge=role,
860
)861
self.graph_database.create_link(
862
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
863
lhs_node=requirement,
864
rhs_node=child_requirement,
865
edge=role,
866
)867
868
cycle_detector = TreeCycleDetector()
869
870
def child_cycle_traverse_(node_id: str) -> List[Any]:
871
node = self.graph_database.get_link_value(
872
link_type=GraphLinkType.UID_TO_NODE,
873
lhs_node=node_id,
874
)875
return list(
876
map(
877
lambda node_: node_.reserved_uid,
878
self.graph_database.get_link_values(
879
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
880
lhs_node=node,
881
),882
)883
)884
885
cycle_detector.check_node(
886
requirement.reserved_uid,
887
child_cycle_traverse_,
888
)889
890
# Mark document and parent document (if different) for re-generation.891
assert document.meta is not None
892
set_file_modification_time(
893
document.meta.input_doc_full_path, datetime.datetime.today()
894
)895
if child_requirement_document != document:
896
assert child_requirement_document.meta is not None
897
set_file_modification_time(
898
child_requirement_document.meta.input_doc_full_path,
899
datetime.datetime.today(),
900
)901
902
def update_with_anchor(self, anchor: Anchor) -> None:
903
# By this time, we know that the validations have passed just before.904
existing_anchor: Optional[Anchor] = (
905
self.graph_database.get_link_value_weak(
906
link_type=GraphLinkType.UID_TO_NODE,
907
lhs_node=anchor.value,
908
)909
)910
if existing_anchor is not None:
911
self.graph_database.delete_link(
912
link_type=GraphLinkType.MID_TO_NODE,
913
lhs_node=existing_anchor.mid,
914
rhs_node=existing_anchor,
915
)916
self.graph_database.delete_link(
917
link_type=GraphLinkType.UID_TO_NODE,
918
lhs_node=existing_anchor.value,
919
rhs_node=existing_anchor,
920
)921
922
self.graph_database.create_link(
923
link_type=GraphLinkType.MID_TO_NODE,
924
lhs_node=anchor.mid,
925
rhs_node=anchor,
926
)927
self.graph_database.create_link(
928
link_type=GraphLinkType.UID_TO_NODE,
929
lhs_node=anchor.value,
930
rhs_node=anchor,
931
)932
933
def update_disconnect_two_documents_if_no_links_left(
934
self, document: SDocDocument, other_document: SDocDocument
935
) -> None:
936
assert document != other_document
937
938
for node, _ in self.document_iterators[document].all_content(
939
print_fragments=False
940
):941
if not isinstance(node, SDocNode):
942
continue943
944
requirement_node: SDocNode = node
945
946
# If a requirement has no UID, it cannot contribute to any relation-based947
# connection between any two documents.948
if requirement_node.reserved_uid is None:
949
continue950
951
requirement_parents: OrderedSet[SDocNode] = (
952
self.graph_database.get_link_values(
953
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
954
lhs_node=requirement_node,
955
)956
)957
958
# If at least one parent or child relation points to the other959
# document, terminate, not deleting the link between documents.960
for parent_requirement in requirement_parents:
961
parent_requirement_document: SDocDocument = assert_cast(
962
parent_requirement.get_document(), SDocDocument
963
)964
if parent_requirement_document == other_document:
965
return966
967
requirement_children = self.graph_database.get_link_values(
968
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
969
lhs_node=requirement_node,
970
)971
972
for child_requirement_ in requirement_children:
973
if child_requirement_.get_document() == other_document:
974
return975
976
def delete_document(self, document: SDocDocument) -> None:
977
assert isinstance(document, SDocDocument), document
978
979
self.graph_database.delete_link(
980
link_type=GraphLinkType.MID_TO_NODE,
981
lhs_node=document.reserved_mid,
982
rhs_node=document,
983
)984
if document.reserved_uid is not None:
985
self.graph_database.delete_link(
986
link_type=GraphLinkType.UID_TO_NODE,
987
lhs_node=document.reserved_uid,
988
rhs_node=document,
989
)990
991
def delete_requirement(self, requirement: SDocNode) -> None:
992
assert isinstance(requirement, SDocNode), SDocNode
993
994
self.graph_database.delete_link(
995
link_type=GraphLinkType.MID_TO_NODE,
996
lhs_node=requirement.reserved_mid,
997
rhs_node=requirement,
998
)999
if requirement.reserved_uid is not None:
1000
self.graph_database.delete_link(
1001
link_type=GraphLinkType.UID_TO_NODE,
1002
lhs_node=requirement.reserved_uid,
1003
rhs_node=requirement,
1004
)1005
# Remove NODE_TO_CHILD_NODES links that point FROM parent nodes TO1006
# this requirement. delete_all_links only removes links where this1007
# requirement is the lhs_node (its own children), leaving stale1008
# reverse entries on parent nodes that block their own deletion.1009
parent_relations = self.graph_database.get_link_values_with_edges(
1010
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
1011
lhs_node=requirement,
1012
)1013
for parent_node_, role_ in parent_relations:
1014
self.graph_database.delete_link(
1015
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
1016
lhs_node=parent_node_,
1017
rhs_node=requirement,
1018
edge=role_,
1019
)1020
self.graph_database.delete_all_links(
1021
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
1022
lhs_node=requirement,
1023
)1024
self.graph_database.delete_all_links(
1025
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
1026
lhs_node=requirement,
1027
)1028
1029
def remove_requirement_parent_uid(
1030
self, requirement: SDocNode, parent_uid: str, role: Optional[str]
1031
) -> None:
1032
assert requirement.reserved_uid is not None
1033
assert isinstance(parent_uid, str), parent_uid
1034
assert role is None or len(role) > 0, role
1035
1036
parent_requirement: SDocNode = self.graph_database.get_link_value(
1037
link_type=GraphLinkType.UID_TO_NODE,
1038
lhs_node=parent_uid,
1039
)1040
1041
self.graph_database.delete_link(
1042
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
1043
lhs_node=requirement,
1044
rhs_node=parent_requirement,
1045
edge=role,
1046
)1047
self.graph_database.delete_link(
1048
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
1049
lhs_node=parent_requirement,
1050
rhs_node=requirement,
1051
edge=role,
1052
)1053
1054
document = assert_cast(requirement.get_document(), SDocDocument)
1055
parent_requirement_document = assert_cast(
1056
parent_requirement.get_document(), SDocDocument
1057
)1058
1059
# If there are no requirements linking between the documents,1060
# remove the link.1061
if document != parent_requirement_document:
1062
self.update_disconnect_two_documents_if_no_links_left(
1063
document, parent_requirement_document
1064
)1065
1066
# Mark document and parent document (if different) for re-generation.1067
assert document.meta is not None
1068
set_file_modification_time(
1069
document.meta.input_doc_full_path, datetime.datetime.today()
1070
)1071
if parent_requirement_document != document:
1072
assert parent_requirement_document.meta is not None
1073
set_file_modification_time(
1074
parent_requirement_document.meta.input_doc_full_path,
1075
datetime.datetime.today(),
1076
)1077
1078
def remove_requirement_child_uid(
1079
self, requirement: SDocNode, child_uid: str, role: Optional[str]
1080
) -> None:
1081
assert requirement.reserved_uid is not None
1082
assert isinstance(child_uid, str), child_uid
1083
assert role is None or len(role) > 0, role
1084
1085
child_requirement: SDocNode = self.graph_database.get_link_value(
1086
link_type=GraphLinkType.UID_TO_NODE,
1087
lhs_node=child_uid,
1088
)1089
document: SDocDocument = assert_cast(
1090
requirement.get_document(), SDocDocument
1091
)1092
child_requirement_document: SDocDocument = assert_cast(
1093
child_requirement.get_document(), SDocDocument
1094
)1095
1096
self.graph_database.delete_link(
1097
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
1098
lhs_node=requirement,
1099
rhs_node=child_requirement,
1100
edge=role,
1101
)1102
self.graph_database.delete_link(
1103
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
1104
lhs_node=child_requirement,
1105
rhs_node=requirement,
1106
edge=role,
1107
)1108
1109
# If there are no requirements linking between the documents,1110
# remove the link.1111
if document != child_requirement_document:
1112
self.update_disconnect_two_documents_if_no_links_left(
1113
document, child_requirement_document
1114
)1115
1116
# Mark document and parent document (if different) for re-generation.1117
assert document.meta is not None
1118
set_file_modification_time(
1119
document.meta.input_doc_full_path, datetime.datetime.today()
1120
)1121
if child_requirement_document != document:
1122
assert child_requirement_document.meta is not None
1123
set_file_modification_time(
1124
child_requirement_document.meta.input_doc_full_path,
1125
datetime.datetime.today(),
1126
)1127
1128
def remove_inline_link(self, inline_link: InlineLink) -> None:
1129
sections_with_incoming_links = (
1130
self.graph_database.get_link_values_reverse(
1131
link_type=GraphLinkType.NODE_TO_INCOMING_LINKS,
1132
rhs_node=inline_link,
1133
)1134
)1135
1136
for node_with_incoming_links in list(sections_with_incoming_links):
1137
self.graph_database.delete_link(
1138
link_type=GraphLinkType.NODE_TO_INCOMING_LINKS,
1139
lhs_node=node_with_incoming_links,
1140
rhs_node=inline_link,
1141
)1142
1143
self.graph_database.delete_link(
1144
link_type=GraphLinkType.MID_TO_NODE,
1145
lhs_node=inline_link.reserved_mid,
1146
rhs_node=inline_link,
1147
)1148
1149
def remove_anchor_by_uid(self, anchor_uid: str) -> None:
1150
anchor: Anchor = self.graph_database.get_link_value(
1151
link_type=GraphLinkType.UID_TO_NODE,
1152
lhs_node=anchor_uid,
1153
)1154
self.graph_database.delete_link(
1155
link_type=GraphLinkType.MID_TO_NODE,
1156
lhs_node=anchor.mid,
1157
rhs_node=anchor,
1158
)1159
self.graph_database.delete_link(
1160
link_type=GraphLinkType.UID_TO_NODE,
1161
lhs_node=anchor_uid,
1162
rhs_node=anchor,
1163
)1164
1165
def validate_node_against_anchors(
1166
self,
1167
*,
1168
node: Union[SDocDocument, SDocNode, None],
1169
new_anchors: List[Anchor],
1170
) -> None:
1171
assert node is None or isinstance(node, (SDocDocument, SDocNode))
1172
assert isinstance(new_anchors, list)
1173
1174
# Check that this node does not have duplicated anchors.1175
new_anchor_uids = set()
1176
for anchor in new_anchors:
1177
if anchor.value in new_anchor_uids:
1178
raise SingleValidationError(
1179
"A node cannot have two anchors with "1180
f"the same identifier: {anchor.value}."
1181
)1182
new_anchor_uids.add(anchor.value)
1183
1184
# If the node is new, the validation is easier: we just need to make1185
# sure that there are no existing anchors with the UIDs brought1186
# by the new anchors.1187
if node is None:
1188
for anchor_uid in new_anchor_uids:
1189
if self.graph_database.has_any_link(
1190
link_type=GraphLinkType.UID_TO_NODE, lhs_node=anchor_uid
1191
):1192
raise SingleValidationError(
1193
"A node contains an anchor that already exists: "1194
f"{anchor_uid}."
1195
)1196
return1197
1198
# If the node is an existing node, we need to check that:1199
# 1) If some of the new anchors already exist in the project tree, we1200
# need to ensure that they exist in the current node, otherwise we1201
# raise a duplication validation error.1202
# 2) If the new anchors do not contain some of the existing node's1203
# current anchors, this means these anchors are being removed. In1204
# that case, we need to check if these anchors are used by any LINKs,1205
# raising a validation if they do.1206
existing_node_anchor_uids = set()
1207
1208
# FIXME: No test reaches this for Section or Document.1209
assert isinstance(node, SDocNode)
1210
for node_anchor_ in node.get_anchors():
1211
existing_node_anchor_uids.add(node_anchor_.value)
1212
1213
#1214
# Validation 1: Assert all UIDs are either:1215
# a) new1216
# b) exist in this node1217
# c) raise a duplication error.1218
#1219
for anchor_uid in new_anchor_uids:
1220
if (
1221
self.graph_database.has_any_link(
1222
link_type=GraphLinkType.UID_TO_NODE, lhs_node=anchor_uid
1223
)1224
and anchor_uid not in existing_node_anchor_uids
1225
):1226
duplicate_anchor: Anchor = self.graph_database.get_link_value(
1227
link_type=GraphLinkType.UID_TO_NODE, lhs_node=anchor_uid
1228
)1229
node_with_duplicate_anchor: SDocNode = assert_cast(
1230
duplicate_anchor.parent_node(), SDocNode
1231
)1232
raise SingleValidationError(
1233
"Another node contains an anchor with the same UID: "1234
f"{anchor_uid}. {node_with_duplicate_anchor.get_display_node_type()}: "
1235
f"{node_with_duplicate_anchor.get_display_title()}."
1236
)1237
1238
#1239
# Validation 2: Check that removed anchors do not have any incoming1240
# links.1241
#1242
to_be_removed_anchor_uids = existing_node_anchor_uids - new_anchor_uids
1243
for to_be_removed_anchor_uid_ in to_be_removed_anchor_uids:
1244
to_be_removed_anchor: Anchor = self.graph_database.get_link_value(
1245
link_type=GraphLinkType.UID_TO_NODE,
1246
lhs_node=to_be_removed_anchor_uid_,
1247
)1248
incoming_links = self.graph_database.get_link_values(
1249
link_type=GraphLinkType.NODE_TO_INCOMING_LINKS,
1250
lhs_node=to_be_removed_anchor.reserved_mid,
1251
)1252
if incoming_links is not None and len(incoming_links) > 0:
1253
incoming_link: InlineLink = incoming_links[0]
1254
incoming_link_parent_node = incoming_link.parent_node()
1255
raise SingleValidationError(
1256
f"Cannot remove anchor with UID "
1257
f"'{incoming_link.link}' because it has incoming "
1258
f"links. Containing node: "
1259
f"{incoming_link_parent_node.get_display_node_type()}: "
1260
f"'{incoming_link_parent_node.get_display_title()}'."
1261
)1262
1263
def validate_can_remove_node(
1264
self, *, node: Union[SDocNode, Anchor]
1265
) -> None:
1266
incoming_links: Optional[List[InlineLink]] = self.get_incoming_links(
1267
node1268
)1269
if incoming_links is not None and len(incoming_links) > 0:
1270
link_list_message = ", ".join(
1271
map(
1272
lambda l_: (
1273
f"'{l_.parent_node().get_display_title()}' -> '{l_.link}'"
1274
),1275
incoming_links,
1276
)1277
)1278
raise SingleValidationError(
1279
f"Cannot remove node '{node.get_display_title()}' with incoming LINKs from: {link_list_message}."
1280
)1281
if isinstance(node, SDocNode):
1282
child_nodes: List[SDocNode] = self.get_children_requirements(node)
1283
if child_nodes is not None and len(child_nodes) > 0:
1284
nodes_list_message = ", ".join(
1285
map(
1286
lambda n_: "'" + n_.get_display_title() + "'",
1287
child_nodes,
1288
)1289
)1290
raise SingleValidationError(
1291
f"Cannot remove node '{node.get_display_title()}' "
1292
f"with incoming relations from:\n{nodes_list_message}."
1293
)1294
1295
def validate_can_remove_document(self, document: SDocDocument) -> None:
1296
errors: List[str] = []
1297
1298
if document.document_is_included():
1299
including_document = document.get_including_document()
1300
if including_document is not None:
1301
errors.append(
1302
f"Cannot remove document '{document.get_display_title()}' because it is included in document '{including_document.get_display_title()}'."
1303
)1304
else:
1305
errors.append(
1306
f"Cannot remove document '{document.get_display_title()}' because it is included in another document."
1307
)1308
1309
incoming_links: Optional[List[InlineLink]] = self.get_incoming_links(
1310
document1311
)1312
if incoming_links is not None and len(incoming_links) > 0:
1313
link_list_message = ", ".join(
1314
map(
1315
lambda l_: (
1316
f"'{l_.parent_node().get_display_title()}' -> '{l_.link}'"
1317
),1318
incoming_links,
1319
)1320
)1321
errors.append(
1322
f"Cannot remove document '{document.get_display_title()}' with incoming LINKs from: {link_list_message}."
1323
)1324
1325
document_iterator = SDocDocumentIterator(document=document)
1326
for document_node_, _ in document_iterator.all_content(
1327
print_fragments=True
1328
):1329
if not isinstance(document_node_, SDocNode):
1330
continue1331
1332
nodes_with_incoming_links = [
1333
document_node_1334
] + document_node_.get_anchors()
1335
for node_ in nodes_with_incoming_links:
1336
try:
1337
self.validate_can_remove_node(node=node_)
1338
except SingleValidationError as exception_:
1339
errors.append(exception_.args[0])
1340
1341
parent_nodes: List[SDocNode] = self.get_parent_requirements(
1342
document_node_1343
)1344
if len(parent_nodes) > 0:
1345
nodes_list_message = ", ".join(
1346
map(
1347
lambda n_: "'" + n_.get_display_title() + "'",
1348
parent_nodes,
1349
)1350
)1351
errors.append(
1352
f"Cannot remove document '{document_node_.get_display_title()}' "
1353
f"with incoming relations from:\n{nodes_list_message}."
1354
)1355
1356
if len(errors) > 0:
1357
raise MultipleValidationErrorAsList("NOT_RELEVANT", errors)
1358
- "6.7.1. Export to HTML content to PDF (HTML2PDF)" (REQUIREMENT)
1359
def clone_to_bundle_document(
1360
self, project_config: ProjectConfig
1361
) -> Tuple["TraceabilityIndex", SDocDocument]:
1362
"""
1363
Clone this traceability index and create a new bundle document.1364
1365
The only use case for this method is the generation of a bundle document.1366
Since the bundle document does not exist on file system, some parameters1367
are set artificially:1368
- The bundle is assumed to be an input file in the root input folder.1369
- The bundle is generated to the root of the output folder (level=0).1370
- Some variables do not contribute (yet) to the final result, so they1371
are marked as NOT_RELEVANT.1372
1373
@relation(SDOC-SRS-51, scope=function)1374
"""1375
traceability_index_copy = deepcopy(self)
1376
bundle_document = SDocDocument(
1377
mid=None,
1378
title=project_config.project_title,
1379
config=None,
1380
view=None,
1381
grammar=None,
1382
section_contents=[],
1383
is_bundle_document=True,
1384
)1385
bundle_document.config = DocumentConfig.default_config(bundle_document)
1386
1387
if (
1388
project_config.bundle_document_uid is not None
1389
and len(project_config.bundle_document_uid) > 0
1390
):1391
bundle_document.config.uid = project_config.bundle_document_uid
1392
1393
if (
1394
project_config.bundle_document_version is not None
1395
and len(project_config.bundle_document_version) > 0
1396
):1397
bundle_document.config.version = (
1398
project_config.bundle_document_version
1399
)1400
1401
if (
1402
project_config.bundle_document_date is not None
1403
and len(project_config.bundle_document_date) > 0
1404
):1405
bundle_document.config.date = project_config.bundle_document_date
1406
1407
bundle_document.meta = DocumentMeta(
1408
level=0,
1409
file_tree_mount_folder="NOT_RELEVANT",
1410
document_filename="bundle.sdoc",
1411
document_filename_base="bundle",
1412
input_doc_full_path="NOT_RELEVANT",
1413
input_doc_rel_path=SDocRelativePath("bundle.sdoc"),
1414
input_doc_dir_rel_path=SDocRelativePath(""),
1415
input_doc_assets_dir_rel_path=SDocRelativePath("NOT_RELEVANT"),
1416
output_document_dir_full_path=project_config.export_output_html_root,
1417
output_document_dir_rel_path=SDocRelativePath(""),
1418
)1419
traceability_index_copy.document_iterators[bundle_document] = (
1420
SDocDocumentIterator(bundle_document)
1421
)1422
for document_ in traceability_index_copy.document_tree.document_list:
1423
# Ignore all included documents. They are anyway included by1424
# the including documents.1425
if document_.document_is_included():
1426
continue1427
1428
assert document_.ng_including_document_reference is not None
1429
document_.ng_including_document_reference.set_document(
1430
bundle_document1431
)1432
bundle_document.section_contents.append(document_)
1433
traceability_index_copy.document_tree.document_list = [bundle_document]
1434
bundle_document.ng_including_document_reference = DocumentReference()
1435
return traceability_index_copy, bundle_document