StrictDoc Documentation
strictdoc/backend/sdoc/models/grammar_element.py
Source file coverage
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_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, 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_role
155
            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_described
171
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_role
180
            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_described
187
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_role
196
            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
            None
227
            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
                pass
275
        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 and
286
        # which are treated as multiline:
287
        # 1) If a node has a content field, e.g., STATEMENT, CONTENT or
288
        # DESCRIPTION, then the fields before it are treated as single-line, and
289
        # 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 the
291
        # single-line and multiline. Note that this also covers the case when
292
        # 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 as
294
        # multiline by setting the multiline_field_index to -1, which is less
295
        # 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
    @staticmethod
312
    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
    @staticmethod
343
    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 or
378
        multiline.
379
 
380
        Currently this method is used for two StrictDoc decisions at the SDoc
381
        markup and GUI levels:
382
 
383
        1) Single-line vs multiline. When writing Python objects from memory to
384
           an SDoc file, StrictDoc must know a field's type in order to serialize
385
           it as either single-line or multiline (>>>...<<<).
386
        2) Meta vs content. Usually, all meta fields are rendered in a separate
387
           block above/before the multiline fields.
388
 
389
        We may introduce a more formal SDoc model to distinguish between
390
        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/2221
394
        """
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 additionally
409
        # check if it is of a non-String type because all non-String types are
410
        # 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 the
421
        # [[NODE]] syntax and didn't enter the corresponding template migration,
422
        # keep the TEXT nodes to have a "plain" style unless their type is
423
        # 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
                break
458
            if field.title in RequirementFieldName.RESERVED_NON_META_FIELDS:
459
                continue
460
            # LEVEL is excluded because the Table screen always displays it
461
            # separately as a table's second column.
462
            if field.title == RequirementFieldName.LEVEL:
463
                continue
464
            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
                continue
478
            if not after_title_or_statement:
479
                continue
480
            # LEVEL is excluded because the Table screen always displays it
481
            # separately as a table's second column.
482
            if field.title == RequirementFieldName.LEVEL:
483
                continue
484
            yield field.title