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