StrictDoc Documentation
strictdoc/backend/sdoc/models/grammar_element.py
Source file coverage
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_described
41
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_described
56
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_described
70
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_described
104
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_described
124
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_described
146
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_role
159
            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_role
164
            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_described
182
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_role
195
            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_role
200
            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_described
208
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_role
221
            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_role
226
            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
            None
258
            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
                pass
306
        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 and
317
        # which are treated as multiline:
318
        # 1) If a node has a content field, e.g., STATEMENT, CONTENT or
319
        # DESCRIPTION, then the fields before it are treated as single-line, and
320
        # 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 the
322
        # single-line and multiline. Note that this also covers the case when
323
        # 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 as
325
        # multiline by setting the multiline_field_index to -1, which is less
326
        # 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
    @staticmethod
343
    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
    @staticmethod
374
    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 or
412
        multiline.
413
 
414
        Currently this method is used for two StrictDoc decisions at the SDoc
415
        markup and GUI levels:
416
 
417
        1) Single-line vs multiline. When writing Python objects from memory to
418
           an SDoc file, StrictDoc must know a field's type in order to serialize
419
           it as either single-line or multiline (>>>...<<<).
420
        2) Meta vs content. Usually, all meta fields are rendered in a separate
421
           block above/before the multiline fields.
422
 
423
        We may introduce a more formal SDoc model to distinguish between
424
        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/2221
428
        """
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 additionally
443
        # check if it is of a non-String type because all non-String types are
444
        # 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 the
455
        # [[NODE]] syntax and didn't enter the corresponding template migration,
456
        # keep the TEXT nodes to have a "plain" style unless their type is
457
        # 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
                break
504
            if field.title in RequirementFieldName.RESERVED_NON_META_FIELDS:
505
                continue
506
            # LEVEL is excluded because the Table screen always displays it
507
            # separately as a table's second column.
508
            if field.title == RequirementFieldName.LEVEL:
509
                continue
510
            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
                continue
524
            if not after_title_or_statement:
525
                continue
526
            # LEVEL is excluded because the Table screen always displays it
527
            # separately as a table's second column.
528
            if field.title == RequirementFieldName.LEVEL:
529
                continue
530
            yield field.title