Path:
strictdoc/backend/sdoc/models/node.py
Lines:
845
Non-empty lines:
724
Non-empty lines covered with requirements:
724 / 724 (100.0%)
Functions:
63
Functions covered by requirements:
63 / 63 (100.0%)
1
"""2
@relation(SDOC-SRS-26, scope=file)3
"""4
5
from collections import OrderedDict
6
from dataclasses import dataclass
7
from enum import Enum
8
from typing import Any, Generator, List, Optional, Tuple, Union
9
10
from strictdoc.backend.sdoc.document_reference import DocumentReference
11
from strictdoc.backend.sdoc.models.anchor import Anchor
12
from strictdoc.backend.sdoc.models.document_config import DocumentConfig
13
from strictdoc.backend.sdoc.models.document_grammar import (
14
DocumentGrammar,
15
)16
from strictdoc.backend.sdoc.models.grammar_element import (
17
GrammarElement,
18
ReferenceType,
19
)20
from strictdoc.backend.sdoc.models.inline_link import InlineLink
21
from strictdoc.backend.sdoc.models.model import (
22
RequirementFieldName,
23
SDocDocumentIF,
24
SDocElementIF,
25
SDocNodeIF,
26
)27
from strictdoc.backend.sdoc.models.reference import (
28
ChildReqReference,
29
ParentReqReference,
30
Reference,
31
)32
from strictdoc.helpers.auto_described import auto_described
33
from strictdoc.helpers.cast import assert_cast
34
from strictdoc.helpers.exception import StrictDocException
35
from strictdoc.helpers.mid import MID
36
from strictdoc.helpers.string import ensure_newline
37
38
39
@dataclass40
class SDocNodeContext:
41
title_number_string: Optional[str] = None
42
ng_level: int = 0
43
44
45
class SDocNodeFieldOrigin(str, Enum):
46
DOCUMENT = "DOCUMENT"
47
SOURCE = "SOURCE"
48
49
@staticmethod50
def all() -> List[str]: # noqa: A003
51
return list(map(lambda c: c.value, SDocNodeFieldOrigin))
52
53
54
@auto_described55
class SDocNodeField:
56
def __init__(
57
self,
58
parent: Optional["SDocNode"],
59
field_name: str,
60
parts: List[Any],
61
multiline__: Optional[str],
62
) -> None:
63
self.parent: Optional[SDocNode] = parent
64
self.field_name: str = field_name
65
self.parts: List[Any] = parts
66
self.multiline: bool = multiline__ is not None and len(multiline__) > 0
67
self.origin: SDocNodeFieldOrigin = SDocNodeFieldOrigin.DOCUMENT
68
69
if (
70
self.multiline
71
and field_name in RequirementFieldName.RESERVED_SINGLELINE_FIELDS
72
):73
raise StrictDocException(
74
f"The node field {field_name} is a reserved field "
75
"and can only be written as a single-line, not multiline, field."76
)77
78
@staticmethod79
def create_from_string(
80
parent: Optional["SDocNode"],
81
field_name: str,
82
field_value: str,
83
multiline: bool,
84
) -> "SDocNodeField":
85
assert isinstance(field_name, str) and len(field_name) > 0, field_name
86
assert isinstance(field_value, str) and len(field_value) > 0, (
87
field_value88
)89
90
return SDocNodeField(
91
parent=parent,
92
field_name=field_name,
93
parts=[field_value],
94
multiline__="multiline" if multiline else None,
95
)96
97
def is_multiline(self) -> bool:
98
return self.multiline
99
100
def get_text_value(self) -> str:
101
text = ""
102
for part in self.parts:
103
if isinstance(part, str):
104
text += part
105
elif isinstance(part, InlineLink):
106
text += "[LINK: "
107
text += part.link
108
text += "]"
109
elif isinstance(part, Anchor):
110
text += "[ANCHOR: "
111
text += part.value
112
if part.has_title:
113
text += ", "
114
text += part.title
115
text += "]"
116
text += "\n"
117
else:
118
raise NotImplementedError(part) # pragma: no cover
119
return text
120
121
def is_document_origin(self) -> bool:
122
return self.origin == SDocNodeFieldOrigin.DOCUMENT
123
124
def mark_as_source_origin(self) -> None:
125
self.origin = SDocNodeFieldOrigin.SOURCE
126
127
128
@auto_described- "1.9. Free text" (REQUIREMENT)
- "1.3. Requirement model fields" (REQUIREMENT)
129
class SDocNode(SDocNodeIF):
130
"""
131
@relation(SDOC-SRS-135, SDOC-SRS-100, scope=class)132
"""133
134
def __init__(
135
self,
136
parent: Union[SDocDocumentIF, SDocNodeIF],
137
node_type: str,
138
fields: List[SDocNodeField],
139
relations: List[Reference],
140
is_composite: bool = False,
141
section_contents: Optional[List[SDocElementIF]] = None,
142
node_type_close: Optional[str] = None,
143
autogen: bool = False,
144
) -> None:
145
assert parent
146
assert isinstance(node_type, str)
147
assert isinstance(relations, list), relations
148
149
self.parent: Union[SDocDocumentIF, SDocNodeIF] = parent
150
151
self.node_type: str = node_type
152
153
if node_type_close is not None and len(node_type_close) > 0:
154
if node_type != node_type_close:
155
raise StrictDocException(
156
"[[NODE]] syntax error: "157
"Opening and closing tags must match: "158
f"opening: {node_type}, closing: {node_type_close}."
159
)160
assert is_composite
161
else:
162
assert not is_composite
163
164
self.is_composite: bool = is_composite
165
166
ordered_fields_lookup: OrderedDict[str, List[SDocNodeField]] = (
167
OrderedDict()
168
)169
170
has_meta: bool = False
171
for field in fields:
172
if (
173
field.field_name
174
not in RequirementFieldName.RESERVED_NON_META_FIELDS
175
):176
has_meta = True
177
ordered_fields_lookup.setdefault(field.field_name, []).append(field)
178
179
self.section_contents: List[SDocElementIF] = (
180
section_contents if section_contents is not None else []
181
)182
183
self.relations: List[Reference] = relations
184
185
# TODO: Is it worth to move this to dedicated Presenter* classes to186
# keep this class textx-only?187
self.has_meta: bool = has_meta
188
189
# This property is only used for validating fields against grammar190
# during TextX parsing and processing.191
self.fields_as_parsed = fields
192
193
self.ordered_fields_lookup: OrderedDict[str, List[SDocNodeField]] = (
194
ordered_fields_lookup195
)196
self.ng_document_reference: Optional[DocumentReference] = None
197
self.ng_including_document_reference: Optional[DocumentReference] = None
198
self.ng_line_start: Optional[int] = None
199
self.ng_line_end: Optional[int] = None
200
self.ng_col_start: Optional[int] = None
201
self.ng_col_end: Optional[int] = None
202
self.ng_byte_start: Optional[int] = None
203
self.ng_byte_end: Optional[int] = None
204
self.context: SDocNodeContext = SDocNodeContext()
205
206
mid: Optional[str] = None
207
mid_fields: Optional[List[SDocNodeField]] = ordered_fields_lookup.get(
208
"MID", None
209
)210
if mid_fields is not None:
211
mid = mid_fields[0].get_text_value()
212
self.reserved_mid: MID = MID(mid) if mid is not None else MID.create()
213
self.mid_permanent: bool = mid is not None
214
215
self.ng_resolved_custom_level: Optional[str] = None
216
self.custom_level: Optional[str] = None
217
if RequirementFieldName.LEVEL in ordered_fields_lookup:
218
level = ordered_fields_lookup[RequirementFieldName.LEVEL][
219
0220
].get_text_value()
221
self.ng_resolved_custom_level = level
222
self.custom_level = level
223
224
self.ng_has_requirements: bool = False
225
226
# Specifies whether a node is created from text or autogenerated, e.g.,227
# from a JUnit XML test report or from reading source file comments.228
# The SDoc writer uses this property to decide whether it shall write229
# autogenerated code to disk.230
self.autogen: bool = autogen
231
232
def get_total_size(self) -> Tuple[int, int, int]:
233
"""
234
Calculate the how many nodes a given node contains.235
236
The returned value is a tuple:237
(total nodes, normative nodes, non-normative nodes)238
"""239
if self.section_contents is None or len(self.section_contents) == 0:
240
is_normative = self.is_normative_node()
241
return 1, int(is_normative), int(not is_normative)
242
total_size = (0, 0, 0)
243
for node_ in self.section_contents:
244
if isinstance(node_, SDocNode):
245
node_total_size = node_.get_total_size()
246
total_size = (
247
total_size[0] + node_total_size[0],
248
total_size[1] + node_total_size[1],
249
total_size[2] + node_total_size[2],
250
)251
return total_size
252
253
@staticmethod254
def create_section(
255
parent: Any, document: SDocDocumentIF, title: str
256
) -> "SDocCompositeNode":
257
node = SDocCompositeNode(
258
parent=parent,
259
node_type="SECTION",
260
fields=[],
261
relations=[],
262
section_contents=[],
263
node_type_close="SECTION",
264
)265
node.ng_including_document_reference = DocumentReference()
266
node.ng_document_reference = DocumentReference()
267
node.ng_document_reference.set_document(document)
268
node.set_field_value(
269
field_name="TITLE",
270
form_field_index=0,
271
value=title,
272
)273
return node
274
275
@staticmethod276
def get_type_string() -> str:
277
return "requirement"
278
279
def get_node_type_string(self) -> Optional[str]:
280
return self.node_type
281
282
def get_display_title(
283
self,
284
include_toc_number: bool = True, # noqa: ARG002
285
) -> str:
286
if self.reserved_title is not None:
287
if (
288
include_toc_number289
and self.context.title_number_string is not None
290
):291
return (
292
f"{self.context.title_number_string}. {self.reserved_title}"
293
)294
return self.reserved_title
295
if self.reserved_uid is not None:
296
return self.reserved_uid
297
if self.node_type == "TEXT":
298
if (
299
isinstance(self.parent, SDocNode)
300
and self.parent.node_type == "SECTION"
301
):302
return f'Text node from section "{self.parent.get_display_title()}"'
303
if isinstance(self.parent, SDocDocumentIF):
304
return f'Text node from document "{self.parent.get_display_title()}"'
305
return f"{self.node_type} with no title/UID"
306
307
@property308
def is_root_included_document(self) -> bool:
309
return False
310
311
@property312
def is_root(self) -> bool:
313
document = assert_cast(self.get_document(), SDocDocumentIF)
314
return document.config.root is True
315
316
def has_multiline_fields(self) -> bool:
317
"""
318
FIXME: It should be possible to avoid calculating this every time.319
"""320
321
document = assert_cast(self.get_document(), SDocDocumentIF)
322
grammar = assert_cast(document.grammar, DocumentGrammar)
323
element: GrammarElement = grammar.elements_by_type[self.node_type]
324
325
for fields_ in self.ordered_fields_lookup.values():
326
for field_ in fields_:
327
if element.is_field_multiline(field_.field_name):
328
return True
329
return False
330
331
def has_any_text_nodes(self) -> bool:
332
# The workaround: hasattr(...) makes mypy happy.333
return any(
334
node_.__class__.__name__ == "SDocNode"
335
and hasattr(node_, "node_type")
336
and node_.node_type == "TEXT"
337
for node_ in self.section_contents
338
)339
340
def has_child_nodes(self) -> bool:
341
return len(self.section_contents) > 0
342
343
#344
# Reserved fields345
#346
347
@property348
def reserved_uid(self) -> Optional[str]:
349
document = assert_cast(self.get_document(), SDocDocumentIF)
350
config = assert_cast(document.config, DocumentConfig)
351
352
return self._get_cached_field(
353
config.get_relation_field(), singleline_only=True
354
)355
356
@reserved_uid.setter
357
def reserved_uid(self, uid: Optional[str]) -> None:
358
document = assert_cast(self.get_document(), SDocDocumentIF)
359
config = assert_cast(document.config, DocumentConfig)
360
361
self.set_field_value(
362
field_name=config.get_relation_field(),
363
form_field_index=0,
364
value=uid,
365
)366
367
@property368
def reserved_status(self) -> Optional[str]:
369
return self._get_cached_field(
370
RequirementFieldName.STATUS, singleline_only=True
371
)372
373
@property374
def reserved_tags(self) -> Optional[List[str]]:
375
if RequirementFieldName.TAGS not in self.ordered_fields_lookup:
376
return None
377
field: SDocNodeField = self.ordered_fields_lookup[
378
RequirementFieldName.TAGS
379
][0]
380
assert not field.is_multiline(), (
381
f"Field {RequirementFieldName.TAGS} must be a single-line field."
382
)383
tags = field.get_text_value().split(", ")
384
return tags
385
386
@property387
def reserved_title(self) -> Optional[str]:
388
return self._get_cached_field(
389
RequirementFieldName.TITLE, singleline_only=True
390
)391
392
def has_reserved_statement(self) -> bool:
393
document = assert_cast(self.get_document(), SDocDocumentIF)
394
grammar = assert_cast(document.grammar, DocumentGrammar)
395
element: GrammarElement = grammar.elements_by_type[self.node_type]
396
return element.content_field[0] in self.ordered_fields_lookup
397
398
@property399
def reserved_statement(self) -> Optional[str]:
400
document = assert_cast(self.get_document(), SDocDocumentIF)
401
grammar = assert_cast(document.grammar, DocumentGrammar)
402
element: GrammarElement = grammar.elements_by_type[self.node_type]
403
return self._get_cached_field(
404
element.content_field[0], singleline_only=False
405
)406
407
@property408
def rationale(self) -> Optional[str]:
409
return self._get_cached_field(
410
RequirementFieldName.RATIONALE, singleline_only=False
411
)412
413
def is_requirement(self) -> bool:
414
return True
415
416
def is_normative_node(self) -> bool:
417
return self.node_type not in ("SECTION", "TEXT")
418
419
def is_text_node(self) -> bool:
420
return self.node_type == "TEXT"
421
422
def is_document(self) -> bool:
423
return False
424
425
def get_document(self) -> Optional[SDocDocumentIF]:
426
assert self.ng_document_reference is not None, self
427
return self.ng_document_reference.get_document()
428
429
def get_including_document(self) -> Optional[SDocDocumentIF]:
430
assert self.ng_including_document_reference is not None
431
return self.ng_including_document_reference.get_document()
432
433
def get_parent_or_including_document(self) -> SDocDocumentIF:
434
assert self.ng_including_document_reference is not None
435
including_document_or_none = (
436
self.ng_including_document_reference.get_document()
437
)438
if including_document_or_none is not None:
439
return including_document_or_none
440
441
assert self.ng_document_reference is not None
442
document: Optional[SDocDocumentIF] = (
443
self.ng_document_reference.get_document()
444
)445
assert document is not None, (
446
"A valid requirement must always have a reference to the document."447
)448
return document
449
450
def get_display_node_type(self) -> str:
451
return "Node"
452
453
def get_debug_info(self) -> str:
454
debug_components: List[str] = []
455
if self.reserved_mid is not None:
456
debug_components.append(f"MID = '{self.reserved_mid}'")
457
if (reserved_uid_ := self.reserved_uid) is not None:
458
debug_components.append(f"UID = '{reserved_uid_}'")
459
if self.reserved_title is not None:
460
debug_components.append(f"TITLE = '{self.reserved_title}'")
461
462
document: Optional[SDocDocumentIF] = self.get_document()
463
if document is not None:
464
debug_components.append(f"document = {document.get_debug_info()}")
465
return f"Requirement({', '.join(debug_components)})"
466
467
def document_is_included(self) -> bool:
468
assert self.ng_including_document_reference is not None
469
return self.ng_including_document_reference.get_document() is not None
470
471
def get_requirement_style_mode(self) -> str:
472
document: SDocDocumentIF = assert_cast(
473
self.get_document(), SDocDocumentIF
474
)475
grammar = assert_cast(document.grammar, DocumentGrammar)
476
element: GrammarElement = grammar.elements_by_type[self.node_type]
477
if node_style := element.get_view_style():
478
return node_style
479
return document.config.get_requirement_style_mode()
480
481
def get_content_field_name(self) -> str:
482
document = assert_cast(self.get_document(), SDocDocumentIF)
483
grammar = assert_cast(document.grammar, DocumentGrammar)
484
485
element: GrammarElement = grammar.elements_by_type[self.node_type]
486
return element.content_field[0]
487
488
def get_content_field(self) -> SDocNodeField:
489
document = assert_cast(self.get_document(), SDocDocumentIF)
490
grammar = assert_cast(document.grammar, DocumentGrammar)
491
492
element: GrammarElement = grammar.elements_by_type[self.node_type]
493
return self.ordered_fields_lookup[element.content_field[0]][0]
494
495
def get_field_by_name(self, field_name: str) -> SDocNodeField:
496
return self.ordered_fields_lookup[field_name][0]
497
498
def get_anchors(self) -> List[Anchor]:
499
this_node_anchors: List[Anchor] = []
500
for field_ in self.enumerate_fields():
501
for field_part_ in field_.parts:
502
if isinstance(field_part_, Anchor):
503
this_node_anchors.append(field_part_)
504
return this_node_anchors
505
506
def get_comment_fields(self) -> List[SDocNodeField]:
507
if RequirementFieldName.COMMENT not in self.ordered_fields_lookup:
508
return []
509
return self.ordered_fields_lookup[RequirementFieldName.COMMENT]
510
511
def get_requirement_references(self, ref_type: str) -> List[Reference]:
512
if len(self.relations) == 0:
513
return []
514
references: List[Reference] = []
515
for reference in self.relations:
516
if reference.ref_type != ref_type:
517
continue518
references.append(reference)
519
return references
520
521
def get_requirement_reference_uids(
522
self,
523
) -> List[Tuple[str, str, Optional[str]]]:
524
if len(self.relations) == 0:
525
return []
526
references: List[Tuple[str, str, Optional[str]]] = []
527
for reference in self.relations:
528
if reference.ref_type == ReferenceType.PARENT:
529
parent_reference: ParentReqReference = assert_cast(
530
reference, ParentReqReference
531
)532
references.append(
533
(534
parent_reference.ref_type,
535
parent_reference.ref_uid,
536
parent_reference.role,
537
)538
)539
elif reference.ref_type == ReferenceType.CHILD:
540
child_reference: ChildReqReference = assert_cast(
541
reference, ChildReqReference
542
)543
references.append(
544
(545
child_reference.ref_type,
546
child_reference.ref_uid,
547
child_reference.role,
548
)549
)550
return references
551
552
def enumerate_fields(self) -> Generator[SDocNodeField, None, None]:
553
requirement_fields = self.ordered_fields_lookup.values()
554
for requirement_field_list in requirement_fields:
555
yield from requirement_field_list
556
557
def enumerate_all_fields(
558
self,
559
) -> Generator[Tuple[SDocNodeField, str, str], None, None]:
560
for field in self.enumerate_fields():
561
meta_field_value = field.get_text_value()
562
yield field, field.field_name, meta_field_value
563
564
def enumerate_meta_fields(
565
self, skip_single_lines: bool = False, skip_multi_lines: bool = False
566
) -> Generator[Tuple[str, SDocNodeField], None, None]:
567
document: SDocDocumentIF = assert_cast(
568
self.get_document(), SDocDocumentIF
569
)570
571
document_grammar: DocumentGrammar = assert_cast(
572
document.grammar, DocumentGrammar
573
)574
575
element: GrammarElement = document_grammar.elements_by_type[
576
self.node_type
577
]578
579
for field in self.enumerate_fields():
580
if (
581
field.field_name
582
in RequirementFieldName.RESERVED_NON_META_FIELDS
583
):584
continue585
586
is_single_line_field = not element.is_field_multiline(
587
field.field_name
588
)589
590
if is_single_line_field and skip_single_lines:
591
continue592
if (not is_single_line_field) and skip_multi_lines:
593
continue594
595
field_human_title = element.fields_map[field.field_name]
596
yield field_human_title.get_field_human_name(), field
597
598
def get_meta_field_value_by_title(self, field_title: str) -> Optional[str]:
599
assert isinstance(field_title, str)
600
if field_title not in self.ordered_fields_lookup:
601
return None
602
field: SDocNodeField = self.ordered_fields_lookup[field_title][0]
603
return field.get_text_value()
604
605
def get_field_human_title(self, field_name: str) -> str:
606
document: SDocDocumentIF = assert_cast(
607
self.get_document(), SDocDocumentIF
608
)609
document_grammar: DocumentGrammar = assert_cast(
610
document.grammar, DocumentGrammar
611
)612
element: GrammarElement = document_grammar.elements_by_type[
613
self.node_type
614
]615
field_human_title = element.fields_map[field_name]
616
return field_human_title.get_field_human_name()
617
618
def get_field_human_title_for_statement(self) -> str:
619
document: SDocDocumentIF = assert_cast(
620
self.get_document(), SDocDocumentIF
621
)622
grammar: DocumentGrammar = assert_cast(
623
document.grammar, DocumentGrammar
624
)625
element: GrammarElement = grammar.elements_by_type[self.node_type]
626
field_human_title = element.fields_map[element.content_field[0]]
627
return field_human_title.get_field_human_name()
628
629
def get_prefix(self) -> Optional[str]:
630
if (
631
own_prefix := self._get_cached_field(
632
RequirementFieldName.PREFIX, singleline_only=True
633
)634
) is not None:
635
if own_prefix == "None":
636
return None
637
return own_prefix
638
639
document: SDocDocumentIF = assert_cast(
640
self.get_document(), SDocDocumentIF
641
)642
grammar: DocumentGrammar = assert_cast(
643
document.grammar, DocumentGrammar
644
)645
element: GrammarElement = grammar.elements_by_type[self.node_type]
646
if (element_prefix := element.property_prefix) is not None:
647
if element_prefix == "None":
648
return None
649
return element_prefix
650
651
# FIXME: Is this a reasonable behavior?652
if (
653
isinstance(self.parent, SDocNode)
654
and self.parent.node_type == "SECTION"
655
):656
if (parent_prefix := self.parent.get_prefix()) is not None:
657
return parent_prefix
658
return document.get_prefix()
659
660
return self.parent.get_prefix()
661
662
def get_prefix_for_new_node(self, node_type: str) -> Optional[str]:
663
assert isinstance(node_type, str) and len(node_type), node_type
664
665
document: SDocDocumentIF = assert_cast(
666
self.get_document(), SDocDocumentIF
667
)668
grammar: DocumentGrammar = assert_cast(
669
document.grammar, DocumentGrammar
670
)671
element: GrammarElement = grammar.elements_by_type[node_type]
672
if (element_prefix := element.property_prefix) is not None:
673
if element_prefix == "None":
674
return None
675
return element_prefix
676
677
return self.get_prefix()
678
679
def is_managed_by_source_code(self) -> bool:
680
"""
681
Helper method to check if a node is partially managed by source code.682
"""683
684
# Is the node entirely generated from source code?685
if self.autogen:
686
return True
687
688
# Check if fields were merged from source-files.689
for field_list in self.ordered_fields_lookup.values():
690
for field in field_list:
691
# If any field did not originate from the document,692
# the node's content is partially managed by source code...693
if not field.is_document_origin():
694
return True
695
696
return False
697
698
def dump_fields_as_parsed(self) -> str:
699
# FIXME:700
# - The name of the method can be improved (used in error messages).701
# - fields can diverge from fields_as_parsed.702
return ", ".join(
703
list(
704
map(
705
lambda r: r.field_name,
706
self.fields_as_parsed,
707
)708
)709
)710
711
def _get_cached_field(
712
self, field_name: str, singleline_only: bool
713
) -> Optional[str]:
714
if field_name not in self.ordered_fields_lookup:
715
return None
716
field: SDocNodeField = self.ordered_fields_lookup[field_name][0]
717
718
if singleline_only and field.is_multiline():
719
raise NotImplementedError(
720
f"Field {field_name} must be a single-line field."
721
)722
723
return field.get_text_value()
724
725
# Below all mutating methods.726
727
def set_field_value(
728
self,
729
*,
730
field_name: str,
731
form_field_index: int,
732
value: Optional[Union[str, SDocNodeField]],
733
) -> None:
734
"""
735
Create or update a field by name with the given value.736
737
The purpose of this purpose is to provide a single-method API for738
updating any field of a requirement. A requirement might use only some739
fields of a document grammar, so an extra exercise done by the method is740
to ensure that an added field that has not been attached to the741
requirement before will be put at the right index.742
"""743
assert isinstance(field_name, str)
744
745
# If a field value is being removed, there is not much to do.746
if value is None or (isinstance(value, str) and len(value) == 0):
747
# Comment is a special because there can be multiple comments.748
# Empty comments are simply ignored and do not show up in the749
# updated requirement.750
if field_name == RequirementFieldName.COMMENT:
751
return752
753
if field_name in self.ordered_fields_lookup:
754
del self.ordered_fields_lookup[field_name]
755
return756
757
# If a field value is being added or updated.758
document: SDocDocumentIF = assert_cast(
759
self.get_document(), SDocDocumentIF
760
)761
grammar: DocumentGrammar = assert_cast(
762
document.grammar, DocumentGrammar
763
)764
element: GrammarElement = grammar.elements_by_type[self.node_type]
765
766
field_index = element.field_titles.index(field_name)
767
768
multiline = element.is_field_multiline(field_name)
769
if multiline and isinstance(value, str):
770
value = ensure_newline(value)
771
772
if field_name in self.ordered_fields_lookup:
773
if len(self.ordered_fields_lookup[field_name]) > form_field_index:
774
self.ordered_fields_lookup[field_name][form_field_index] = (
775
SDocNodeField.create_from_string(
776
self,
777
field_name=field_name,
778
field_value=value,
779
multiline=multiline,
780
)781
if isinstance(value, str)
782
else value
783
)784
else:
785
self.ordered_fields_lookup[field_name].insert(
786
form_field_index,
787
SDocNodeField.create_from_string(
788
self,
789
field_name=field_name,
790
field_value=value,
791
multiline=multiline,
792
)793
if isinstance(value, str)
794
else value,
795
)796
return797
798
new_ordered_fields_lookup = OrderedDict()
799
for field_title in element.field_titles[:field_index]:
800
if field_title in self.ordered_fields_lookup:
801
new_ordered_fields_lookup[field_title] = (
802
self.ordered_fields_lookup[field_title]
803
)804
new_ordered_fields_lookup[field_name] = [
805
SDocNodeField.create_from_string(
806
self,
807
field_name=field_name,
808
field_value=value,
809
multiline=multiline,
810
)811
if isinstance(value, str)
812
else value
813
]814
after_field_index = field_index + 1
815
for field_title in element.field_titles[after_field_index:]:
816
if field_title in self.ordered_fields_lookup:
817
new_ordered_fields_lookup[field_title] = (
818
self.ordered_fields_lookup[field_title]
819
)820
self.ordered_fields_lookup = new_ordered_fields_lookup
821
self._update_has_meta()
822
823
def _update_has_meta(self) -> None:
824
has_meta: bool = False
825
for field in self.enumerate_fields():
826
if (
827
field.field_name
828
not in RequirementFieldName.RESERVED_NON_META_FIELDS
829
):830
has_meta = True
831
self.has_meta = has_meta
832
833
834
@auto_described835
class SDocCompositeNode(SDocNode):
836
"""
837
@relation(SDOC-SRS-99, scope=class)838
"""839
840
def __init__(
841
self,
842
parent: Union[SDocDocumentIF, SDocNodeIF],
843
**fields: Any,
844
) -> None:
845
super().__init__(parent, **fields, is_composite=True)