StrictDoc Documentation
strictdoc/backend/sdoc/writer.py
Source file coverage
Path:
strictdoc/backend/sdoc/writer.py
Lines:
586
Non-empty lines:
508
Non-empty lines covered with requirements:
508 / 508 (100.0%)
Functions:
10
Functions covered by requirements:
10 / 10 (100.0%)
1
import os.path
2
from pathlib import Path
3
from typing import Dict, List, Optional, Tuple
4
 
5
from strictdoc.backend.sdoc.models.document import SDocDocument
6
from strictdoc.backend.sdoc.models.document_config import DocumentConfig
7
from strictdoc.backend.sdoc.models.document_from_file import DocumentFromFile
8
from strictdoc.backend.sdoc.models.document_grammar import DocumentGrammar
9
from strictdoc.backend.sdoc.models.document_view import (
10
    DefaultViewElement,
11
    ViewElement,
12
)
13
from strictdoc.backend.sdoc.models.grammar_element import (
14
    GrammarElementFieldMultipleChoice,
15
    GrammarElementFieldSingleChoice,
16
    GrammarElementFieldString,
17
    GrammarElementFieldTag,
18
    GrammarElementFieldType,
19
    GrammarElementRelationType,
20
    RequirementFieldType,
21
)
22
from strictdoc.backend.sdoc.models.model import (
23
    SDocElementIF,
24
)
25
from strictdoc.backend.sdoc.models.node import (
26
    SDocCompositeNode,
27
    SDocNode,
28
)
29
from strictdoc.backend.sdoc.models.reference import (
30
    ChildReqReference,
31
    FileReference,
32
    ParentReqReference,
33
    Reference,
34
)
35
from strictdoc.backend.sdoc.node_filter import NodeFilter
36
from strictdoc.core.document_iterator import SDocDocumentIterator
37
from strictdoc.core.document_meta import DocumentMeta
38
from strictdoc.core.project_config import ProjectConfig
39
from strictdoc.helpers.cast import assert_cast
40
from strictdoc.helpers.string import ensure_newline
41
 
42
 
43
class SDWriter:
44
    def __init__(
45
        self,
46
        project_config: ProjectConfig,
47
        node_filter: Optional[NodeFilter] = None,
48
    ) -> None:
49
        self.project_config: ProjectConfig = project_config
50
        self.node_filter: Optional[NodeFilter] = node_filter
51
 
52
    def write_to_file(self, document: SDocDocument) -> None:
53
        document_content, fragments_dict = self.write_with_fragments(document)
54
 
55
        document_meta: DocumentMeta = assert_cast(document.meta, DocumentMeta)
56
 
57
        with open(
58
            document_meta.input_doc_full_path, "w", encoding="utf8"
59
        ) as output_file:
60
            output_file.write(document_content)
61
 
62
        path_to_output_file_dir = os.path.dirname(
63
            document_meta.input_doc_full_path
64
        )
65
        Path(path_to_output_file_dir).mkdir(parents=True, exist_ok=True)
66
 
67
        for fragment_path_, fragment_content_ in fragments_dict.items():
68
            path_to_output_fragment = os.path.join(
69
                path_to_output_file_dir, fragment_path_
70
            )
71
            with open(path_to_output_fragment, "w", encoding="utf8") as file_:
72
                file_.write(fragment_content_)
73
 
74
    def write(self, document: SDocDocument) -> str:
75
        document_output, _ = self.write_with_fragments(document)
76
        return document_output
77
 
78
    def write_with_fragments(
79
        self, document: SDocDocument
80
    ) -> Tuple[str, Dict[str, str]]:
81
        fragments_dict: Dict[str, str] = {}
82
 
83
        document_iterator = SDocDocumentIterator(document)
84
        output = ""
85
 
86
        output += "[DOCUMENT]"
87
        output += "\n"
88
 
89
        if document.mid_permanent or document.config.enable_mid:
90
            output += "MID: "
91
            output += document.reserved_mid
92
            output += "\n"
93
 
94
        output += "TITLE: "
95
        output += document.title
96
        output += "\n"
97
 
98
        document_config: DocumentConfig = document.config
99
        if document_config:
100
            uid = document_config.uid
101
            if uid:
102
                output += f"UID: {uid}"
103
                output += "\n"
104
 
105
            version = document_config.version
106
            if version:
107
                output += f"VERSION: {version}"
108
                output += "\n"
109
 
110
            date = document_config.date
111
            if date is not None:
112
                output += f"DATE: {date}"
113
                output += "\n"
114
 
115
            classification = document_config.classification
116
            if classification is not None:
117
                output += f"CLASSIFICATION: {classification}"
118
                output += "\n"
119
 
120
            requirement_prefix = document_config.requirement_prefix
121
            if requirement_prefix is not None:
122
                output += f"PREFIX: {requirement_prefix}"
123
                output += "\n"
124
 
125
            root = document_config.root
126
            if root is not None:
127
                output += "ROOT: "
128
                output += "True" if root else "False"
129
                output += "\n"
130
 
131
            enable_mid = document_config.enable_mid
132
            relation_field = document_config.relation_field
133
            markup = document_config.markup
134
            auto_levels_specified = document_config.ng_auto_levels_specified
135
            layout = document_config.layout
136
            requirement_style = document_config.requirement_style
137
            requirement_in_toc = document_config.requirement_in_toc
138
            default_view = document_config.default_view
139
 
140
            if (
141
                enable_mid is not None
142
                or relation_field is not None
143
                or markup is not None
144
                or auto_levels_specified
145
                or layout is not None
146
                or requirement_style is not None
147
                or requirement_in_toc is not None
148
                or default_view is not None
149
            ):
150
                output += "OPTIONS:"
151
                output += "\n"
152
 
153
                if enable_mid is not None:
154
                    output += "  ENABLE_MID: "
155
                    output += "True" if enable_mid else "False"
156
                    output += "\n"
157
 
158
                if relation_field is not None:
159
                    output += "  RELATION_FIELD: "
160
                    output += relation_field
161
                    output += "\n"
162
 
163
                if markup is not None:
164
                    output += "  MARKUP: "
165
                    output += markup
166
                    output += "\n"
167
 
168
                if auto_levels_specified:
169
                    output += "  AUTO_LEVELS: "
170
                    output += "On" if document_config.auto_levels else "Off"
171
                    output += "\n"
172
 
173
                if layout is not None:
174
                    output += "  LAYOUT: "
175
                    output += layout
176
                    output += "\n"
177
 
178
                if requirement_style is not None:
179
                    output += "  VIEW_STYLE: "
180
                    output += requirement_style
181
                    output += "\n"
182
 
183
                if requirement_in_toc is not None:
184
                    output += "  NODE_IN_TOC: "
185
                    output += requirement_in_toc
186
                    output += "\n"
187
 
188
                if default_view is not None:
189
                    output += "  DEFAULT_VIEW: "
190
                    output += default_view
191
                    output += "\n"
192
 
193
            custom_metadata = document_config.custom_metadata
194
            if custom_metadata is not None:
195
                output += "METADATA:"
196
                output += "\n"
197
 
198
                for keyvalue_pair in custom_metadata.entries:
199
                    if (
200
                        keyvalue_pair.key is not None
201
                        and keyvalue_pair.value is not None
202
                    ):
203
                        output += (
204
                            "  "
205
                            + keyvalue_pair.key
206
                            + ": "
207
                            + keyvalue_pair.value
208
                        )
209
                        output += "\n"
210
 
211
        document_view = document.view
212
        assert len(document_view.views) > 0
213
        if not isinstance(document_view.views[0], DefaultViewElement):
214
            views = document_view.views
215
            output += "VIEWS:"
216
            output += "\n"
217
            for view in views:
218
                output += f"- ID: {view.view_id}\n"
219
                if view.name is not None:
220
                    output += f"  NAME: {view.name}\n"
221
                assert len(view.tags) > 0
222
                output += "  TAGS:\n"
223
                for tag in view.tags:
224
                    output += f"  - OBJECT_TYPE: {tag.object_type}\n"
225
                    output += "    VISIBLE_FIELDS:\n"
226
                    for field in tag.visible_fields:
227
                        output += f"    - NAME: {field.name}\n"
228
                        placement = field.placement
229
                        if placement:
230
                            output += f"      PLACEMENT: {placement}\n"
231
                hidden_tags = view.hidden_tags
232
                if hidden_tags is not None and len(hidden_tags) > 0:
233
                    output += "  HIDDEN_TAGS:\n"
234
                    for hidden_tag in hidden_tags:
235
                        output += f"  - {hidden_tag.hidden_tag}\n"
236
 
237
        assert document.grammar is not None
238
        document_grammar: DocumentGrammar = document.grammar
239
        if not document_grammar.is_default:
240
            output += "\n[GRAMMAR]\n"
241
            if document_grammar.import_from_file is not None:
242
                output += (
243
                    f"IMPORT_FROM_FILE: {document_grammar.import_from_file}\n"
244
                )
245
            else:
246
                output += "ELEMENTS:\n"
247
                for element in document_grammar.elements:
248
                    output += "- TAG: "
249
                    output += element.tag
250
                    output += "\n"
251
 
252
                    if (
253
                        element.property_is_composite is not None
254
                        or element.property_prefix is not None
255
                        or element.property_view_style is not None
256
                    ):
257
                        output += "  PROPERTIES:\n"
258
                        if element.property_is_composite is not None:
259
                            output += "    IS_COMPOSITE: "
260
                            output += (
261
                                "True"
262
                                if element.property_is_composite
263
                                else "False"
264
                            )
265
                            output += "\n"
266
                        if element.property_prefix is not None:
267
                            output += "    PREFIX: "
268
                            output += element.property_prefix
269
                            output += "\n"
270
                        if element.property_view_style is not None:
271
                            output += "    VIEW_STYLE: "
272
                            output += element.property_view_style
273
                            output += "\n"
274
 
275
                    output += "  FIELDS:\n"
276
                    for grammar_field in element.fields:
277
                        output += SDWriter._print_grammar_field_type(
278
                            grammar_field
279
                        )
280
 
281
                    relations: List[GrammarElementRelationType] = (
282
                        element.relations
283
                    )
284
 
285
                    if len(relations) > 0:
286
                        output += "  RELATIONS:\n"
287
 
288
                        for element_relation in relations:
289
                            output += (
290
                                f"  - TYPE: {element_relation.relation_type}\n"
291
                            )
292
                            if element_relation.relation_role is not None:
293
                                output += f"    ROLE: {element_relation.relation_role}\n"
294
 
295
        output += "\n"
296
 
297
        output += self._print_node(
298
            document,
299
            document,
300
            document_iterator,
301
        )
302
        output = output.rstrip()
303
        output += "\n"
304
 
305
        return output, fragments_dict
306
 
307
    def _print_node(
308
        self,
309
        root_node: SDocElementIF,
310
        document: SDocDocument,
311
        document_iterator: SDocDocumentIterator,
312
    ) -> str:
313
        # Currently, auto-generated nodes are never written back to file system.
314
        # We could revisit this in the future.
315
        if root_node.autogen:
316
            return ""
317
 
318
        assert isinstance(document_iterator, SDocDocumentIterator), (
319
            document_iterator
320
        )
321
 
322
        if isinstance(root_node, SDocDocument):
323
            output = ""
324
 
325
            for node_ in root_node.section_contents:
326
                if (
327
                    self.node_filter is not None
328
                    and not self.node_filter.is_whitelisted(node_)
329
                ):
330
                    continue
331
                output += self._print_node(
332
                    node_,
333
                    document,
334
                    document_iterator=document_iterator,
335
                )
336
            return output
337
 
338
        if isinstance(root_node, DocumentFromFile):
339
            document_from_file: DocumentFromFile = assert_cast(
340
                root_node, DocumentFromFile
341
            )
342
            return self._print_document_from_file(document_from_file)
343
 
344
        if isinstance(root_node, SDocNode):
345
            output = ""
346
 
347
            if (
348
                isinstance(root_node, SDocCompositeNode)
349
                or root_node.is_composite
350
            ):
351
                output += "[["
352
                output += root_node.node_type
353
                output += "]]\n"
354
            else:
355
                output += "["
356
                output += root_node.node_type
357
                output += "]\n"
358
 
359
            output += self._print_requirement_fields(
360
                section_content=root_node, document=document
361
            )
362
            output += "\n"
363
 
364
            if (
365
                isinstance(root_node, SDocCompositeNode)
366
                or root_node.is_composite
367
            ):
368
                if root_node.section_contents is not None:
369
                    for node_ in root_node.section_contents:
370
                        if (
371
                            self.node_filter is not None
372
                            and not self.node_filter.is_whitelisted(node_)
373
                        ):
374
                            continue
375
                        output += self._print_node(
376
                            node_, document, document_iterator=document_iterator
377
                        )
378
 
379
                if (
380
                    isinstance(root_node, SDocCompositeNode)
381
                    or root_node.is_composite
382
                ):
383
                    output += "[[/"
384
                    output += root_node.node_type
385
                    output += "]]\n"
386
                    output += "\n"
387
 
388
            return output
389
 
390
        raise AssertionError("Must not reach here")  # pragma: no cover
391
 
392
    @staticmethod
393
    def _print_document_from_file(document_from_file: DocumentFromFile) -> str:
394
        assert isinstance(document_from_file, DocumentFromFile)
395
        output = ""
396
        output += "[DOCUMENT_FROM_FILE]"
397
        output += "\n"
398
 
399
        output += "FILE: "
400
        output += document_from_file.file
401
        output += "\n\n"
402
 
403
        return output
404
 
405
    def _print_requirement_fields(
406
        self, section_content: SDocNode, document: SDocDocument
407
    ) -> str:
408
        assert document.grammar is not None
409
 
410
        output = ""
411
 
412
        current_view: ViewElement = document.view.get_current_view(
413
            self.project_config.view
414
        )
415
        element = document.grammar.elements_by_type[section_content.node_type]
416
 
417
        for element_field in element.fields:
418
            field_name = element_field.title
419
            if field_name not in section_content.ordered_fields_lookup:
420
                if field_name == "MID" and document.config.enable_mid:
421
                    output += "MID: "
422
                    output += section_content.reserved_mid
423
                    output += "\n"
424
                continue
425
            if not current_view.includes_field(
426
                section_content.node_type, field_name
427
            ):
428
                continue
429
            fields = section_content.ordered_fields_lookup[field_name]
430
 
431
            for field in fields:
432
                if not field.is_document_origin():
433
                    continue
434
 
435
                field_value = field.get_text_value()
436
                assert len(field_value) > 0
437
 
438
                if field.is_multiline():
439
                    output += f"{field_name}: >>>"
440
                    output += "\n"
441
                    if field_value != "\n":
442
                        output += ensure_newline(field_value)
443
                    output += "<<<"
444
                    output += "\n"
445
                else:
446
                    output += f"{field_name}: "
447
                    output += field_value
448
                    output += "\n"
449
 
450
        output += SDWriter._print_requirement_relations(section_content)
451
 
452
        return output
453
 
454
    @staticmethod
455
    def _print_grammar_field_type(
456
        grammar_field: GrammarElementFieldType,
457
    ) -> str:
458
        output = ""
459
        output += "  - TITLE: "
460
        output += grammar_field.title
461
        output += "\n"
462
        if grammar_field.human_title is not None:
463
            output += "    HUMAN_TITLE: "
464
            output += grammar_field.human_title
465
            output += "\n"
466
        output += "    TYPE: "
467
 
468
        if isinstance(grammar_field, GrammarElementFieldString):
469
            output += RequirementFieldType.STRING
470
        elif isinstance(grammar_field, GrammarElementFieldSingleChoice):
471
            output += RequirementFieldType.SINGLE_CHOICE
472
            output += "("
473
            output += ", ".join(grammar_field.get_unprocessed_options())
474
            output += ")"
475
        elif isinstance(grammar_field, GrammarElementFieldMultipleChoice):
476
            output += RequirementFieldType.MULTIPLE_CHOICE
477
            output += "("
478
            output += ", ".join(grammar_field.options)
479
            output += ")"
480
        elif isinstance(grammar_field, GrammarElementFieldTag):
481
            output += RequirementFieldType.TAG
482
        else:
483
            raise NotImplementedError from None  # pragma: no cover
484
 
485
        output += "\n"
486
        output += "    REQUIRED: "
487
        output += "True" if grammar_field.required else "False"
488
        output += "\n"
489
        return output
490
 
491
    @classmethod
492
    def _print_requirement_relations(cls, requirement: SDocNode) -> str:
493
        assert isinstance(requirement, SDocNode)
494
 
495
        if len(requirement.relations) == 0:
496
            return ""
497
 
498
        output = "RELATIONS:\n"
499
 
500
        reference: Reference
501
        for reference in requirement.relations:
502
            output += "- TYPE: "
503
            output += reference.ref_type
504
            output += "\n"
505
 
506
            if isinstance(reference, FileReference):
507
                ref: FileReference = reference
508
                if ref.role is not None:
509
                    output += "  ROLE: "
510
                    output += ref.role
511
                    output += "\n"
512
                file_format = ref.get_file_format()
513
                if file_format:
514
                    output += "  FORMAT: "
515
                    output += file_format
516
                    output += "\n"
517
 
518
                if ref.g_file_entry.g_deprecated_file_path is not None:
519
                    output += "  VALUE: "
520
                else:
521
                    output += "  PATH: "
522
 
523
                output += ref.get_posix_path()
524
                output += "\n"
525
                if (
526
                    ref.g_file_entry.deprecated_function is None
527
                    and ref.g_file_entry.deprecated_clazz is None
528
                    and ref.g_file_entry.element is not None
529
                    and ref.g_file_entry.id is not None
530
                ):
531
                    output += "  ELEMENT: "
532
                    output += ref.g_file_entry.element
533
                    output += "\n"
534
                    output += "  ID: "
535
                    output += ref.g_file_entry.id
536
                    output += "\n"
537
                elif (
538
                    ref.g_file_entry.deprecated_function is None
539
                    and ref.g_file_entry.deprecated_clazz is None
540
                    and ref.g_file_entry.id is not None
541
                    and ref.g_file_entry.line_range is None
542
                ):
543
                    output += "  ID: "
544
                    output += ref.g_file_entry.id
545
                    output += "\n"
546
                elif ref.g_file_entry.line_range is not None:
547
                    output += "  LINE_RANGE: "
548
                    output += str(ref.g_file_entry.line_range[0])
549
                    output += ", "
550
                    output += str(ref.g_file_entry.line_range[1])
551
                    output += "\n"
552
                elif ref.g_file_entry.deprecated_function is not None:
553
                    output += "  FUNCTION: "
554
                    output += str(ref.g_file_entry.deprecated_function)
555
                    output += "\n"
556
                elif ref.g_file_entry.deprecated_clazz is not None:
557
                    output += "  CLASS: "
558
                    output += str(ref.g_file_entry.deprecated_clazz)
559
                    output += "\n"
560
 
561
                if ref.g_file_entry.hash is not None:
562
                    output += "  HASH: "
563
                    output += str(ref.g_file_entry.hash)
564
                    output += "\n"
565
 
566
            elif isinstance(reference, ParentReqReference):
567
                parent_reference: ParentReqReference = reference
568
                output += "  VALUE: "
569
                output += parent_reference.ref_uid
570
                output += "\n"
571
                if parent_reference.role is not None:
572
                    output += "  ROLE: "
573
                    output += parent_reference.role
574
                    output += "\n"
575
            elif isinstance(reference, ChildReqReference):
576
                child_reference: ChildReqReference = reference
577
                output += "  VALUE: "
578
                output += child_reference.ref_uid
579
                output += "\n"
580
                if child_reference.role is not None:
581
                    output += "  ROLE: "
582
                    output += child_reference.role
583
                    output += "\n"
584
            else:
585
                raise AssertionError("Must not reach here.")  # pragma: no cover
586
        return output