Path:
strictdoc/backend/sdoc/models/grammar_element.py
Lines:
531
Non-empty lines:
463
Non-empty lines covered with requirements:
463 / 463 (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
*,
245
parent: Any,
246
tag: str,
247
property_is_composite: str,
248
property_prefix: str,
249
property_view_style: str,
250
fields: List[GrammarElementFieldType],
251
relations: List[GrammarElementRelationType],
252
) -> None:
253
self.parent: Any = parent
254
self.tag: str = tag
255
256
assert property_is_composite in ("", "True", "False")
257
self.property_is_composite: Optional[bool] = (
258
None259
if property_is_composite == ""
260
else (property_is_composite == "True")
261
)262
263
self.property_prefix: Optional[str] = (
264
property_prefix if property_prefix not in (None, "") else None
265
)266
267
assert property_view_style in (
268
"",
269
"Plain",
270
"Narrative",
271
"Simple",
272
"Inline",
273
"Table",
274
"Zebra",
275
)276
self.property_view_style: Optional[str] = (
277
property_view_style if property_view_style != "" else None
278
)279
self.property_view_style_lower: Optional[str] = (
280
property_view_style.lower() if property_view_style != "" else None
281
)282
283
self.fields: List[GrammarElementFieldType] = fields
284
285
self.relations: List[GrammarElementRelationType] = (
286
relations if relations is not None and len(relations) > 0 else []
287
)288
289
fields_map: OrderedDict[str, GrammarElementField] = OrderedDict()
290
291
statement_field: Optional[Tuple[str, int]] = None
292
description_field: Optional[Tuple[str, int]] = None
293
content_field: Optional[Tuple[str, int]] = None
294
for field_idx_, field_ in enumerate(fields):
295
fields_map[field_.title] = field_
296
if field_.title == RequirementFieldName.STATEMENT:
297
statement_field = (RequirementFieldName.STATEMENT, field_idx_)
298
elif field_.title == "DESCRIPTION":
299
description_field = (
300
RequirementFieldName.DESCRIPTION,
301
field_idx_,
302
)303
elif field_.title == "CONTENT":
304
content_field = (RequirementFieldName.CONTENT, field_idx_)
305
else:
306
pass307
self.fields_map: Dict[str, GrammarElementField] = fields_map
308
309
self.field_titles: List[str] = list(
310
map(lambda field__: field__.title, self.fields)
311
)312
313
self.content_field: Tuple[str, int] = (
314
statement_field or description_field or content_field or ("", -1)
315
)316
317
# The following rule governs which fields are treated as single-line and318
# which are treated as multiline:319
# 1) If a node has a content field, e.g., STATEMENT, CONTENT or320
# DESCRIPTION, then the fields before it are treated as single-line, and321
# the fields starting from it and after it are treated as multiline.322
# 2) If there is no content field, use TITLE as a boundary between the323
# single-line and multiline. Note that this also covers the case when324
# the TITLE is the last field in which case there are no multiline fields.325
# 3) If there is no content field and no TITLE, treat all fields as326
# multiline by setting the multiline_field_index to -1, which is less327
# than any valid field index.328
if self.content_field[1] != -1:
329
multiline_field_index = self.content_field[1]
330
else:
331
try:
332
multiline_field_index = (
333
self.get_field_titles().index("TITLE") + 1
334
)335
except ValueError:
336
multiline_field_index = -1
337
self._multiline_field_index: int = multiline_field_index
338
339
self.mid: MID = MID.create()
340
self.ng_line_start: Optional[int] = None
341
self.ng_col_start: Optional[int] = None
342
343
@staticmethod344
def create_default(tag: str) -> "GrammarElement":
345
return GrammarElement(
346
parent=None,
347
tag=tag,
348
property_is_composite="",
349
property_prefix="",
350
property_view_style="",
351
fields=[
352
GrammarElementFieldString(
353
parent=None,
354
title="UID",
355
human_title=None,
356
required="False",
357
),358
GrammarElementFieldString(
359
parent=None,
360
title="TITLE",
361
human_title=None,
362
required="False",
363
),364
GrammarElementFieldString(
365
parent=None,
366
title="STATEMENT",
367
human_title=None,
368
required="False",
369
),370
],371
relations=[],
372
)373
374
@staticmethod375
def create_default_relations(
376
parent: "GrammarElement",
377
include_child: bool = False,
378
) -> List[GrammarElementRelationType]:
379
relations: List[GrammarElementRelationType] = [
380
GrammarElementRelationParent(
381
parent=parent,
382
relation_type="Parent",
383
relation_role=None,
384
reverse_relation_role=None,
385
),386
]387
if include_child:
388
relations.append(
389
GrammarElementRelationChild(
390
parent=parent,
391
relation_type="Child",
392
relation_role=None,
393
reverse_relation_role=None,
394
)395
)396
relations.append(
397
GrammarElementRelationFile(
398
parent=parent,
399
relation_type="File",
400
relation_role=None,
401
reverse_relation_role=None,
402
)403
)404
return relations
405
406
def is_field_multiline(self, field_name: str) -> bool:
407
field_index = self.field_titles.index(field_name)
408
return self.is_field_idx_multiline(field_index)
409
410
def is_field_idx_multiline(self, field_idx: int) -> bool:
411
"""
412
Determine whether a given field shall be treated as single-line or413
multiline.414
415
Currently this method is used for two StrictDoc decisions at the SDoc416
markup and GUI levels:417
418
1) Single-line vs multiline. When writing Python objects from memory to419
an SDoc file, StrictDoc must know a field's type in order to serialize420
it as either single-line or multiline (>>>...<<<).421
2) Meta vs content. Usually, all meta fields are rendered in a separate422
block above/before the multiline fields.423
424
We may introduce a more formal SDoc model to distinguish between425
single-line vs multilines and meta vs content fields in the future.426
See this discussion for more details:427
428
https://github.com/strictdoc-project/strictdoc/discussions/2221429
"""430
431
field_name = self.field_titles[field_idx]
432
433
# Reserved single-line/meta fields can never be multiline.434
if field_name in RequirementFieldName.RESERVED_SINGLELINE_FIELDS:
435
return False
436
437
# If there is none of TITLE-STATEMENT-DESCRIPTION-CONTENT present, i.e.,438
# multiline_field_index is -1, every field will be treated as multiline.439
is_multiline = self._multiline_field_index <= field_idx
440
if not is_multiline:
441
return False
442
443
# If the field should be multiline according to its index, we additionally444
# check if it is of a non-String type because all non-String types are445
# currently treated as single-line.446
field = self.fields_map[field_name]
447
if field.gef_type != RequirementFieldType.STRING:
448
return False
449
450
return True
451
452
def get_view_style(self) -> Optional[str]:
453
if self.property_view_style_lower is not None:
454
return self.property_view_style_lower
455
# For backward compatibility with older versions that didn't have the456
# [[NODE]] syntax and didn't enter the corresponding template migration,457
# keep the TEXT nodes to have a "plain" style unless their type is458
# specified by the grammar.459
if self.tag == "TEXT":
460
return "plain"
461
return None
462
463
def get_relation_types(self) -> List[str]:
464
return list(
465
map(lambda relation_: relation_.relation_type, self.relations)
466
)467
468
def get_field_titles(self) -> List[str]:
469
return self.field_titles
470
471
def get_tag_lower(self) -> str:
472
return self.tag.lower()
473
474
def has_relation_type_role(
475
self, relation_type: str, relation_role: Optional[str]
476
) -> bool:
477
assert relation_role is None or len(relation_role) > 0
478
for relation_ in self.relations:
479
if (
480
relation_.relation_type == relation_type
481
and relation_.relation_role == relation_role
482
):483
return True
484
return False
485
486
def get_relation_reverse_role(
487
self, relation_type: str, relation_role: Optional[str]
488
) -> Optional[str]:
489
assert relation_role is None or len(relation_role) > 0
490
for relation_ in self.relations:
491
if (
492
relation_.relation_type == relation_type
493
and relation_.relation_role == relation_role
494
):495
return relation_.reverse_relation_role
496
return None
497
498
def enumerate_table_meta_field_titles(self) -> Generator[str, None, None]:
499
for field in self.fields:
500
if field.title in (
501
RequirementFieldName.TITLE,
502
RequirementFieldName.STATEMENT,
503
):504
break505
if field.title in RequirementFieldName.RESERVED_NON_META_FIELDS:
506
continue507
# LEVEL is excluded because the Table screen always displays it508
# separately as a table's second column.509
if field.title == RequirementFieldName.LEVEL:
510
continue511
yield field.title
512
513
def enumerate_table_non_reserved_content_field_titles(
514
self,
515
) -> Generator[str, None, None]:
516
after_title_or_statement = False
517
for field in self.fields:
518
if field.title in (
519
RequirementFieldName.TITLE,
520
RequirementFieldName.STATEMENT,
521
):522
after_title_or_statement = True
523
if field.title in RequirementFieldName.RESERVED_NON_META_FIELDS:
524
continue525
if not after_title_or_statement:
526
continue527
# LEVEL is excluded because the Table screen always displays it528
# separately as a table's second column.529
if field.title == RequirementFieldName.LEVEL:
530
continue531
yield field.title