Path:
strictdoc/backend/sdoc/models/grammar_element.py
Lines:
484
Non-empty lines:
417
Non-empty lines covered with requirements:
417 / 417 (100.0%)
Functions:
35
Functions covered by requirements:
35 / 35 (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, parent: Any, relation_type: str, relation_role: Optional[str]
149
) -> None:
150
assert relation_type == "Parent"
151
self.parent: Any = parent
152
self.relation_type: str = relation_type
153
self.relation_role: Optional[str] = (
154
relation_role155
if relation_role is not None and len(relation_role) > 0
156
else None
157
)158
self.mid: MID = MID.create()
159
160
def __eq__(self, other: Any) -> bool:
161
if not isinstance(other, GrammarElementRelationParent):
162
raise AssertionError(self, other) # pragma: no cover
163
return (
164
self.mid == other.mid
165
and self.relation_type == other.relation_type
166
and self.relation_role == other.relation_role
167
)168
169
170
@auto_described171
class GrammarElementRelationChild:
172
def __init__(
173
self, parent: Any, relation_type: str, relation_role: Optional[str]
174
):175
assert relation_type == "Child"
176
self.parent: Any = parent
177
self.relation_type = relation_type
178
self.relation_role: Optional[str] = (
179
relation_role180
if relation_role is not None and len(relation_role) > 0
181
else None
182
)183
self.mid: MID = MID.create()
184
185
186
@auto_described187
class GrammarElementRelationFile:
188
def __init__(
189
self, parent: Any, relation_type: str, relation_role: Optional[str]
190
):191
assert relation_type == "File"
192
self.parent: Any = parent
193
self.relation_type = relation_type
194
self.relation_role: Optional[str] = (
195
relation_role196
if relation_role is not None and len(relation_role) > 0
197
else None
198
)199
self.mid: MID = MID.create()
200
201
202
GrammarElementRelationType = Union[
203
GrammarElementRelationParent,
204
GrammarElementRelationChild,
205
GrammarElementRelationFile,
206
]207
208
209
@auto_described()
210
class GrammarElement:
211
def __init__(
212
self,
213
parent: Any,
214
tag: str,
215
property_is_composite: str,
216
property_prefix: str,
217
property_view_style: str,
218
fields: List[GrammarElementFieldType],
219
relations: List[GrammarElementRelationType],
220
) -> None:
221
self.parent: Any = parent
222
self.tag: str = tag
223
224
assert property_is_composite in ("", "True", "False")
225
self.property_is_composite: Optional[bool] = (
226
None227
if property_is_composite == ""
228
else (property_is_composite == "True")
229
)230
231
self.property_prefix: Optional[str] = (
232
property_prefix if property_prefix not in (None, "") else None
233
)234
235
assert property_view_style in (
236
"",
237
"Plain",
238
"Narrative",
239
"Simple",
240
"Inline",
241
"Table",
242
"Zebra",
243
)244
self.property_view_style: Optional[str] = (
245
property_view_style if property_view_style != "" else None
246
)247
self.property_view_style_lower: Optional[str] = (
248
property_view_style.lower() if property_view_style != "" else None
249
)250
251
self.fields: List[GrammarElementFieldType] = fields
252
253
self.relations: List[GrammarElementRelationType] = (
254
relations if relations is not None and len(relations) > 0 else []
255
)256
257
fields_map: OrderedDict[str, GrammarElementField] = OrderedDict()
258
259
statement_field: Optional[Tuple[str, int]] = None
260
description_field: Optional[Tuple[str, int]] = None
261
content_field: Optional[Tuple[str, int]] = None
262
for field_idx_, field_ in enumerate(fields):
263
fields_map[field_.title] = field_
264
if field_.title == RequirementFieldName.STATEMENT:
265
statement_field = (RequirementFieldName.STATEMENT, field_idx_)
266
elif field_.title == "DESCRIPTION":
267
description_field = (
268
RequirementFieldName.DESCRIPTION,
269
field_idx_,
270
)271
elif field_.title == "CONTENT":
272
content_field = (RequirementFieldName.CONTENT, field_idx_)
273
else:
274
pass275
self.fields_map: Dict[str, GrammarElementField] = fields_map
276
277
self.field_titles: List[str] = list(
278
map(lambda field__: field__.title, self.fields)
279
)280
281
self.content_field: Tuple[str, int] = (
282
statement_field or description_field or content_field or ("", -1)
283
)284
285
# The following rule governs which fields are treated as single-line and286
# which are treated as multiline:287
# 1) If a node has a content field, e.g., STATEMENT, CONTENT or288
# DESCRIPTION, then the fields before it are treated as single-line, and289
# the fields starting from it and after it are treated as multiline.290
# 2) If there is no content field, use TITLE as a boundary between the291
# single-line and multiline. Note that this also covers the case when292
# the TITLE is the last field in which case there are no multiline fields.293
# 3) If there is no content field and no TITLE, treat all fields as294
# multiline by setting the multiline_field_index to -1, which is less295
# than any valid field index.296
if self.content_field[1] != -1:
297
multiline_field_index = self.content_field[1]
298
else:
299
try:
300
multiline_field_index = (
301
self.get_field_titles().index("TITLE") + 1
302
)303
except ValueError:
304
multiline_field_index = -1
305
self._multiline_field_index: int = multiline_field_index
306
307
self.mid: MID = MID.create()
308
self.ng_line_start: Optional[int] = None
309
self.ng_col_start: Optional[int] = None
310
311
@staticmethod312
def create_default(tag: str) -> "GrammarElement":
313
return GrammarElement(
314
parent=None,
315
tag=tag,
316
property_is_composite="",
317
property_prefix="",
318
property_view_style="",
319
fields=[
320
GrammarElementFieldString(
321
parent=None,
322
title="UID",
323
human_title=None,
324
required="False",
325
),326
GrammarElementFieldString(
327
parent=None,
328
title="TITLE",
329
human_title=None,
330
required="False",
331
),332
GrammarElementFieldString(
333
parent=None,
334
title="STATEMENT",
335
human_title=None,
336
required="False",
337
),338
],339
relations=[],
340
)341
342
@staticmethod343
def create_default_relations(
344
parent: "GrammarElement",
345
include_child: bool = False,
346
) -> List[GrammarElementRelationType]:
347
relations: List[GrammarElementRelationType] = [
348
GrammarElementRelationParent(
349
parent=parent,
350
relation_type="Parent",
351
relation_role=None,
352
),353
]354
if include_child:
355
relations.append(
356
GrammarElementRelationChild(
357
parent=parent,
358
relation_type="Child",
359
relation_role=None,
360
)361
)362
relations.append(
363
GrammarElementRelationFile(
364
parent=parent,
365
relation_type="File",
366
relation_role=None,
367
)368
)369
return relations
370
371
def is_field_multiline(self, field_name: str) -> bool:
372
field_index = self.field_titles.index(field_name)
373
return self.is_field_idx_multiline(field_index)
374
375
def is_field_idx_multiline(self, field_idx: int) -> bool:
376
"""
377
Determine whether a given field shall be treated as single-line or378
multiline.379
380
Currently this method is used for two StrictDoc decisions at the SDoc381
markup and GUI levels:382
383
1) Single-line vs multiline. When writing Python objects from memory to384
an SDoc file, StrictDoc must know a field's type in order to serialize385
it as either single-line or multiline (>>>...<<<).386
2) Meta vs content. Usually, all meta fields are rendered in a separate387
block above/before the multiline fields.388
389
We may introduce a more formal SDoc model to distinguish between390
single-line vs multilines and meta vs content fields in the future.391
See this discussion for more details:392
393
https://github.com/strictdoc-project/strictdoc/discussions/2221394
"""395
396
field_name = self.field_titles[field_idx]
397
398
# Reserved single-line/meta fields can never be multiline.399
if field_name in RequirementFieldName.RESERVED_SINGLELINE_FIELDS:
400
return False
401
402
# If there is none of TITLE-STATEMENT-DESCRIPTION-CONTENT present, i.e.,403
# multiline_field_index is -1, every field will be treated as multiline.404
is_multiline = self._multiline_field_index <= field_idx
405
if not is_multiline:
406
return False
407
408
# If the field should be multiline according to its index, we additionally409
# check if it is of a non-String type because all non-String types are410
# currently treated as single-line.411
field = self.fields_map[field_name]
412
if field.gef_type != RequirementFieldType.STRING:
413
return False
414
415
return True
416
417
def get_view_style(self) -> Optional[str]:
418
if self.property_view_style_lower is not None:
419
return self.property_view_style_lower
420
# For backward compatibility with older versions that didn't have the421
# [[NODE]] syntax and didn't enter the corresponding template migration,422
# keep the TEXT nodes to have a "plain" style unless their type is423
# specified by the grammar.424
if self.tag == "TEXT":
425
return "plain"
426
return None
427
428
def get_relation_types(self) -> List[str]:
429
return list(
430
map(lambda relation_: relation_.relation_type, self.relations)
431
)432
433
def get_field_titles(self) -> List[str]:
434
return self.field_titles
435
436
def get_tag_lower(self) -> str:
437
return self.tag.lower()
438
439
def has_relation_type_role(
440
self, relation_type: str, relation_role: Optional[str]
441
) -> bool:
442
assert relation_role is None or len(relation_role) > 0
443
for relation_ in self.relations:
444
if (
445
relation_.relation_type == relation_type
446
and relation_.relation_role == relation_role
447
):448
return True
449
return False
450
451
def enumerate_table_meta_field_titles(self) -> Generator[str, None, None]:
452
for field in self.fields:
453
if field.title in (
454
RequirementFieldName.TITLE,
455
RequirementFieldName.STATEMENT,
456
):457
break458
if field.title in RequirementFieldName.RESERVED_NON_META_FIELDS:
459
continue460
# LEVEL is excluded because the Table screen always displays it461
# separately as a table's second column.462
if field.title == RequirementFieldName.LEVEL:
463
continue464
yield field.title
465
466
def enumerate_table_non_reserved_content_field_titles(
467
self,
468
) -> Generator[str, None, None]:
469
after_title_or_statement = False
470
for field in self.fields:
471
if field.title in (
472
RequirementFieldName.TITLE,
473
RequirementFieldName.STATEMENT,
474
):475
after_title_or_statement = True
476
if field.title in RequirementFieldName.RESERVED_NON_META_FIELDS:
477
continue478
if not after_title_or_statement:
479
continue480
# LEVEL is excluded because the Table screen always displays it481
# separately as a table's second column.482
if field.title == RequirementFieldName.LEVEL:
483
continue484
yield field.title