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