Path:
strictdoc/backend/sdoc/models/grammar_element.py
Lines:
530
Non-empty lines:
462
Non-empty lines covered with requirements:
462 / 462 (100.0%)
Functions:
36
Functions covered by requirements:
36 / 36 (100.0%)
1
"""2
@relation(SDOC-SRS-21, scope=file)3
"""4
5
from collections import OrderedDict
6
from typing import Any, Dict, Generator, List, Optional, Tuple, Union
7
8
from strictdoc.backend.sdoc.models.model import (
9
RequirementFieldName,
10
)11
from strictdoc.helpers.auto_described import auto_described
12
from strictdoc.helpers.mid import MID
13
14
15
class RequirementFieldType:
16
STRING = "String"
17
SINGLE_CHOICE = "SingleChoice"
18
MULTIPLE_CHOICE = "MultipleChoice"
19
TAG = "Tag"
20
21
22
class GrammarReferenceType:
23
PARENT_REQ_REFERENCE = "ParentReqReference"
24
CHILD_REQ_REFERENCE = "ChildReqReference"
25
FILE_REFERENCE = "FileReference"
26
27
28
class ReferenceType:
29
PARENT = "Parent"
30
CHILD = "Child"
31
FILE = "File"
32
33
GRAMMAR_REFERENCE_TYPE_MAP = {
34
PARENT: GrammarReferenceType.PARENT_REQ_REFERENCE,
35
CHILD: GrammarReferenceType.CHILD_REQ_REFERENCE,
36
FILE: GrammarReferenceType.FILE_REFERENCE,
37
}38
39
40
@auto_described41
class GrammarElementField:
42
def __init__(self) -> None:
43
self.title: str = ""
44
self.human_title: Optional[str] = None
45
self.gef_type: str = ""
46
self.required: bool = False
47
self.mid: MID = MID.create()
48
49
def get_field_human_name(self) -> str:
50
if self.human_title is not None:
51
return self.human_title
52
return self.title
53
54
55
@auto_described56
class GrammarElementFieldString(GrammarElementField):
57
def __init__(
58
self, parent: Any, title: str, human_title: Optional[str], required: str
59
) -> None:
60
super().__init__()
61
self.parent: Any = parent
62
self.title: str = title
63
self.human_title: Optional[str] = human_title
64
self.gef_type = RequirementFieldType.STRING
65
self.required: bool = required == "True"
66
self.mid: MID = MID.create()
67
68
69
@auto_described70
class GrammarElementFieldSingleChoice(GrammarElementField):
71
def __init__(
72
self,
73
parent: Any,
74
title: str,
75
human_title: Optional[str],
76
options: List[str],
77
required: str,
78
) -> None:
79
super().__init__()
80
self.parent: Any = parent
81
self.title: str = title
82
self.human_title: Optional[str] = human_title
83
self.gef_type = RequirementFieldType.SINGLE_CHOICE
84
85
processed_options = []
86
for option_ in options:
87
processed_options.append(option_.strip('"'))
88
self.options: List[str] = processed_options
89
90
self.required: bool = required == "True"
91
self.mid: MID = MID.create()
92
93
def get_unprocessed_options(self) -> List[str]:
94
unprocessed_options = []
95
for option_ in self.options:
96
if any(char_ in option_ for char_ in ["(", ")"]):
97
unprocessed_options.append('"' + option_ + '"')
98
else:
99
unprocessed_options.append(option_)
100
return unprocessed_options
101
102
103
@auto_described104
class GrammarElementFieldMultipleChoice(GrammarElementField):
105
def __init__(
106
self,
107
parent: Any,
108
title: str,
109
human_title: Optional[str],
110
options: List[str],
111
required: str,
112
) -> None:
113
super().__init__()
114
self.parent: Any = parent
115
self.title: str = title
116
self.human_title: Optional[str] = human_title
117
self.gef_type = RequirementFieldType.MULTIPLE_CHOICE
118
self.options: List[str] = options
119
self.required: bool = required == "True"
120
self.mid: MID = MID.create()
121
122
123
@auto_described124
class GrammarElementFieldTag(GrammarElementField):
125
def __init__(
126
self, parent: Any, title: str, human_title: Optional[str], required: str
127
) -> None:
128
super().__init__()
129
self.parent: Any = parent
130
self.title: str = title
131
self.human_title: Optional[str] = human_title
132
self.gef_type = RequirementFieldType.TAG
133
self.required: bool = required == "True"
134
self.mid: MID = MID.create()
135
136
137
GrammarElementFieldType = Union[
138
GrammarElementFieldString,
139
GrammarElementFieldSingleChoice,
140
GrammarElementFieldMultipleChoice,
141
GrammarElementFieldTag,
142
]143
144
145
@auto_described146
class GrammarElementRelationParent: # noqa: PLW1641
147
def __init__(
148
self,
149
parent: Any,
150
relation_type: str,
151
relation_role: Optional[str],
152
reverse_relation_role: Optional[str] = None,
153
) -> None:
154
assert relation_type == "Parent"
155
self.parent: Any = parent
156
self.relation_type: str = relation_type
157
self.relation_role: Optional[str] = (
158
relation_role159
if relation_role is not None and len(relation_role) > 0
160
else None
161
)162
self.reverse_relation_role: Optional[str] = (
163
reverse_relation_role164
if reverse_relation_role is not None
165
and len(reverse_relation_role) > 0
166
else None
167
)168
self.mid: MID = MID.create()
169
170
def __eq__(self, other: Any) -> bool:
171
if not isinstance(other, GrammarElementRelationParent):
172
raise AssertionError(self, other) # pragma: no cover
173
return (
174
self.mid == other.mid
175
and self.relation_type == other.relation_type
176
and self.relation_role == other.relation_role
177
and self.reverse_relation_role == other.reverse_relation_role
178
)179
180
181
@auto_described182
class GrammarElementRelationChild:
183
def __init__(
184
self,
185
parent: Any,
186
relation_type: str,
187
relation_role: Optional[str],
188
reverse_relation_role: Optional[str] = None,
189
):190
assert relation_type == "Child"
191
self.parent: Any = parent
192
self.relation_type = relation_type
193
self.relation_role: Optional[str] = (
194
relation_role195
if relation_role is not None and len(relation_role) > 0
196
else None
197
)198
self.reverse_relation_role: Optional[str] = (
199
reverse_relation_role200
if reverse_relation_role is not None
201
and len(reverse_relation_role) > 0
202
else None
203
)204
self.mid: MID = MID.create()
205
206
207
@auto_described208
class GrammarElementRelationFile:
209
def __init__(
210
self,
211
parent: Any,
212
relation_type: str,
213
relation_role: Optional[str],
214
reverse_relation_role: Optional[str] = None,
215
):216
assert relation_type == "File"
217
self.parent: Any = parent
218
self.relation_type = relation_type
219
self.relation_role: Optional[str] = (
220
relation_role221
if relation_role is not None and len(relation_role) > 0
222
else None
223
)224
self.reverse_relation_role: Optional[str] = (
225
reverse_relation_role226
if reverse_relation_role is not None
227
and len(reverse_relation_role) > 0
228
else None
229
)230
self.mid: MID = MID.create()
231
232
233
GrammarElementRelationType = Union[
234
GrammarElementRelationParent,
235
GrammarElementRelationChild,
236
GrammarElementRelationFile,
237
]238
239
240
@auto_described()
241
class GrammarElement:
242
def __init__(
243
self,
244
parent: Any,
245
tag: str,
246
property_is_composite: str,
247
property_prefix: str,
248
property_view_style: str,
249
fields: List[GrammarElementFieldType],
250
relations: List[GrammarElementRelationType],
251
) -> None:
252
self.parent: Any = parent
253
self.tag: str = tag
254
255
assert property_is_composite in ("", "True", "False")
256
self.property_is_composite: Optional[bool] = (
257
None258
if property_is_composite == ""
259
else (property_is_composite == "True")
260
)261
262
self.property_prefix: Optional[str] = (
263
property_prefix if property_prefix not in (None, "") else None
264
)265
266
assert property_view_style in (
267
"",
268
"Plain",
269
"Narrative",
270
"Simple",
271
"Inline",
272
"Table",
273
"Zebra",
274
)275
self.property_view_style: Optional[str] = (
276
property_view_style if property_view_style != "" else None
277
)278
self.property_view_style_lower: Optional[str] = (
279
property_view_style.lower() if property_view_style != "" else None
280
)281
282
self.fields: List[GrammarElementFieldType] = fields
283
284
self.relations: List[GrammarElementRelationType] = (
285
relations if relations is not None and len(relations) > 0 else []
286
)287
288
fields_map: OrderedDict[str, GrammarElementField] = OrderedDict()
289
290
statement_field: Optional[Tuple[str, int]] = None
291
description_field: Optional[Tuple[str, int]] = None
292
content_field: Optional[Tuple[str, int]] = None
293
for field_idx_, field_ in enumerate(fields):
294
fields_map[field_.title] = field_
295
if field_.title == RequirementFieldName.STATEMENT:
296
statement_field = (RequirementFieldName.STATEMENT, field_idx_)
297
elif field_.title == "DESCRIPTION":
298
description_field = (
299
RequirementFieldName.DESCRIPTION,
300
field_idx_,
301
)302
elif field_.title == "CONTENT":
303
content_field = (RequirementFieldName.CONTENT, field_idx_)
304
else:
305
pass306
self.fields_map: Dict[str, GrammarElementField] = fields_map
307
308
self.field_titles: List[str] = list(
309
map(lambda field__: field__.title, self.fields)
310
)311
312
self.content_field: Tuple[str, int] = (
313
statement_field or description_field or content_field or ("", -1)
314
)315
316
# The following rule governs which fields are treated as single-line and317
# which are treated as multiline:318
# 1) If a node has a content field, e.g., STATEMENT, CONTENT or319
# DESCRIPTION, then the fields before it are treated as single-line, and320
# the fields starting from it and after it are treated as multiline.321
# 2) If there is no content field, use TITLE as a boundary between the322
# single-line and multiline. Note that this also covers the case when323
# the TITLE is the last field in which case there are no multiline fields.324
# 3) If there is no content field and no TITLE, treat all fields as325
# multiline by setting the multiline_field_index to -1, which is less326
# than any valid field index.327
if self.content_field[1] != -1:
328
multiline_field_index = self.content_field[1]
329
else:
330
try:
331
multiline_field_index = (
332
self.get_field_titles().index("TITLE") + 1
333
)334
except ValueError:
335
multiline_field_index = -1
336
self._multiline_field_index: int = multiline_field_index
337
338
self.mid: MID = MID.create()
339
self.ng_line_start: Optional[int] = None
340
self.ng_col_start: Optional[int] = None
341
342
@staticmethod343
def create_default(tag: str) -> "GrammarElement":
344
return GrammarElement(
345
parent=None,
346
tag=tag,
347
property_is_composite="",
348
property_prefix="",
349
property_view_style="",
350
fields=[
351
GrammarElementFieldString(
352
parent=None,
353
title="UID",
354
human_title=None,
355
required="False",
356
),357
GrammarElementFieldString(
358
parent=None,
359
title="TITLE",
360
human_title=None,
361
required="False",
362
),363
GrammarElementFieldString(
364
parent=None,
365
title="STATEMENT",
366
human_title=None,
367
required="False",
368
),369
],370
relations=[],
371
)372
373
@staticmethod374
def create_default_relations(
375
parent: "GrammarElement",
376
include_child: bool = False,
377
) -> List[GrammarElementRelationType]:
378
relations: List[GrammarElementRelationType] = [
379
GrammarElementRelationParent(
380
parent=parent,
381
relation_type="Parent",
382
relation_role=None,
383
reverse_relation_role=None,
384
),385
]386
if include_child:
387
relations.append(
388
GrammarElementRelationChild(
389
parent=parent,
390
relation_type="Child",
391
relation_role=None,
392
reverse_relation_role=None,
393
)394
)395
relations.append(
396
GrammarElementRelationFile(
397
parent=parent,
398
relation_type="File",
399
relation_role=None,
400
reverse_relation_role=None,
401
)402
)403
return relations
404
405
def is_field_multiline(self, field_name: str) -> bool:
406
field_index = self.field_titles.index(field_name)
407
return self.is_field_idx_multiline(field_index)
408
409
def is_field_idx_multiline(self, field_idx: int) -> bool:
410
"""
411
Determine whether a given field shall be treated as single-line or412
multiline.413
414
Currently this method is used for two StrictDoc decisions at the SDoc415
markup and GUI levels:416
417
1) Single-line vs multiline. When writing Python objects from memory to418
an SDoc file, StrictDoc must know a field's type in order to serialize419
it as either single-line or multiline (>>>...<<<).420
2) Meta vs content. Usually, all meta fields are rendered in a separate421
block above/before the multiline fields.422
423
We may introduce a more formal SDoc model to distinguish between424
single-line vs multilines and meta vs content fields in the future.425
See this discussion for more details:426
427
https://github.com/strictdoc-project/strictdoc/discussions/2221428
"""429
430
field_name = self.field_titles[field_idx]
431
432
# Reserved single-line/meta fields can never be multiline.433
if field_name in RequirementFieldName.RESERVED_SINGLELINE_FIELDS:
434
return False
435
436
# If there is none of TITLE-STATEMENT-DESCRIPTION-CONTENT present, i.e.,437
# multiline_field_index is -1, every field will be treated as multiline.438
is_multiline = self._multiline_field_index <= field_idx
439
if not is_multiline:
440
return False
441
442
# If the field should be multiline according to its index, we additionally443
# check if it is of a non-String type because all non-String types are444
# currently treated as single-line.445
field = self.fields_map[field_name]
446
if field.gef_type != RequirementFieldType.STRING:
447
return False
448
449
return True
450
451
def get_view_style(self) -> Optional[str]:
452
if self.property_view_style_lower is not None:
453
return self.property_view_style_lower
454
# For backward compatibility with older versions that didn't have the455
# [[NODE]] syntax and didn't enter the corresponding template migration,456
# keep the TEXT nodes to have a "plain" style unless their type is457
# specified by the grammar.458
if self.tag == "TEXT":
459
return "plain"
460
return None
461
462
def get_relation_types(self) -> List[str]:
463
return list(
464
map(lambda relation_: relation_.relation_type, self.relations)
465
)466
467
def get_field_titles(self) -> List[str]:
468
return self.field_titles
469
470
def get_tag_lower(self) -> str:
471
return self.tag.lower()
472
473
def has_relation_type_role(
474
self, relation_type: str, relation_role: Optional[str]
475
) -> bool:
476
assert relation_role is None or len(relation_role) > 0
477
for relation_ in self.relations:
478
if (
479
relation_.relation_type == relation_type
480
and relation_.relation_role == relation_role
481
):482
return True
483
return False
484
485
def get_relation_reverse_role(
486
self, relation_type: str, relation_role: Optional[str]
487
) -> Optional[str]:
488
assert relation_role is None or len(relation_role) > 0
489
for relation_ in self.relations:
490
if (
491
relation_.relation_type == relation_type
492
and relation_.relation_role == relation_role
493
):494
return relation_.reverse_relation_role
495
return None
496
497
def enumerate_table_meta_field_titles(self) -> Generator[str, None, None]:
498
for field in self.fields:
499
if field.title in (
500
RequirementFieldName.TITLE,
501
RequirementFieldName.STATEMENT,
502
):503
break504
if field.title in RequirementFieldName.RESERVED_NON_META_FIELDS:
505
continue506
# LEVEL is excluded because the Table screen always displays it507
# separately as a table's second column.508
if field.title == RequirementFieldName.LEVEL:
509
continue510
yield field.title
511
512
def enumerate_table_non_reserved_content_field_titles(
513
self,
514
) -> Generator[str, None, None]:
515
after_title_or_statement = False
516
for field in self.fields:
517
if field.title in (
518
RequirementFieldName.TITLE,
519
RequirementFieldName.STATEMENT,
520
):521
after_title_or_statement = True
522
if field.title in RequirementFieldName.RESERVED_NON_META_FIELDS:
523
continue524
if not after_title_or_statement:
525
continue526
# LEVEL is excluded because the Table screen always displays it527
# separately as a table's second column.528
if field.title == RequirementFieldName.LEVEL:
529
continue530
yield field.title