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_field279
)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_iterator320
)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
continue331
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
continue375
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
@staticmethod393
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
continue425
if not current_view.includes_field(
426
section_content.node_type, field_name
427
):428
continue429
fields = section_content.ordered_fields_lookup[field_name]
430
431
for field in fields:
432
if not field.is_document_origin():
433
continue434
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
@staticmethod455
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
@classmethod492
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