Path:
strictdoc/core/format.py
Lines:
201
Non-empty lines:
173
Non-empty lines covered with requirements:
173 / 173 (100.0%)
Functions:
22
Functions covered by requirements:
22 / 22 (100.0%)
1
"""2
The Format abstraction: top-level abstraction for all formats that3
StrictDoc can export to and/or import from.4
5
@relation(SDOC-SRS-119, scope=file)6
"""7
8
import argparse
9
from abc import ABC, abstractmethod
10
from dataclasses import dataclass
11
from typing import TYPE_CHECKING, List, Optional, Tuple
12
13
from strictdoc.helpers.string import create_safe_document_file_name
14
15
if TYPE_CHECKING:
16
from strictdoc.backend.sdoc.models.document import SDocDocument
17
from strictdoc.backend.sdoc.models.document_grammar import DocumentGrammar
18
from strictdoc.commands.convert_config import ConvertCommandConfig
19
from strictdoc.core.file_system.file_tree import File
20
from strictdoc.core.project_config import ProjectConfig
21
from strictdoc.core.traceability_index import TraceabilityIndex
22
from strictdoc.export.html.document_type import DocumentType
23
from strictdoc.export.html.html_templates import HTMLTemplates
24
from strictdoc.helpers.parallelizer import Parallelizer
25
26
27
@dataclass28
class ExportContext:
29
"""
30
Shared, per-export() call state that Format.export_complete_tree()31
implementations pull from. html_templates/bundle_* fields are only32
populated when an HTML-based format (html, html2pdf) was requested, since33
building them is expensive and only those formats need them.34
"""35
36
project_config: "ProjectConfig"
37
traceability_index: "TraceabilityIndex"
38
parallelizer: "Parallelizer"
39
html_templates: Optional["HTMLTemplates"] = None
40
bundle_traceability_index: Optional["TraceabilityIndex"] = None
41
bundle_document: Optional["SDocDocument"] = None
42
43
44
class Format(ABC):
45
@staticmethod46
@abstractmethod47
def handles() -> List[str]:
48
"""The CLI/config handle(s) this Format reacts to, e.g. ['html']."""
49
raise NotImplementedError
50
51
@staticmethod52
@abstractmethod53
def supported_extensions() -> List[str]:
54
raise NotImplementedError
55
56
@staticmethod57
@abstractmethod58
def supports_import() -> bool:
59
raise NotImplementedError
60
61
@staticmethod62
@abstractmethod63
def supports_export() -> bool:
64
raise NotImplementedError
65
66
@staticmethod67
@abstractmethod68
def supports_read() -> bool:
69
raise NotImplementedError
70
71
@staticmethod72
def read_extensions() -> List[str]:
73
"""
74
Extensions this Format auto-discovers/reads as native DocumentFinder75
tree-building input. Distinct from supported_extensions(), since not76
every extension a Format is generally associated with is also one it77
natively reads today (e.g. ReqIFFormat reads ".reqif" but not78
".reqifz", even though supported_extensions() lists both).79
"""80
return []
81
82
def read_from_file(
83
self, doc_file: "File", project_config: "ProjectConfig"
84
) -> "SDocDocument":
85
raise NotImplementedError(
86
f"{self.__class__.__name__} does not support reading."
87
)88
89
@staticmethod90
@abstractmethod91
def supports_grammar() -> bool:
92
raise NotImplementedError
93
94
@staticmethod95
def grammar_extensions() -> List[str]:
96
return []
97
98
def read_grammar(
99
self, doc_file: "File", project_config: "ProjectConfig"
100
) -> "DocumentGrammar":
101
raise NotImplementedError(
102
f"{self.__class__.__name__} does not support reading a grammar."
103
)104
105
@abstractmethod106
def export_complete_tree(self, context: ExportContext, handle: str) -> None:
107
raise NotImplementedError
108
109
def export_single_document(
110
self,
111
context: ExportContext,
112
document: "SDocDocument",
113
specific_documents: Optional[Tuple["DocumentType", ...]] = None,
114
) -> None:
115
raise NotImplementedError(
116
f"{self.__class__.__name__} does not support exporting a "
117
f"single document."
118
)119
120
@classmethod121
def handle_for_extension(cls, extension: str) -> Optional[str]:
122
"""
123
Map a file extension to one of this Format's handles(), for124
`convert --input-format` auto-discovery. Pairs handles()/125
supported_extensions() positionally when both lists are the same126
length (e.g. ReqIFFormat's ["reqif-sdoc", "reqifz-sdoc"] <->127
[".reqif", ".reqifz"]); otherwise falls back to the first handle.128
Returns None if this Format has no CLI handle at all (e.g.129
JUnitXMLFormat) or doesn't support the given extension.130
"""131
handles = cls.handles()
132
if not handles:
133
return None
134
extensions = cls.supported_extensions()
135
if extension not in extensions:
136
return None
137
if len(handles) == len(extensions):
138
return handles[extensions.index(extension)]
139
return handles[0]
140
141
@staticmethod142
def supports_convert_output() -> bool:
143
"""Whether `convert --output-format` may target this Format."""
144
return False
145
146
def write_converted_document(
147
self,
148
document: "SDocDocument",
149
output_dir: str,
150
filename_stem: str,
151
project_config: "ProjectConfig",
152
) -> None:
153
raise NotImplementedError(
154
f"{self.__class__.__name__} does not support being a convert "
155
f"output format."
156
)157
158
def import_output_filename_stem(
159
self,
160
document: "SDocDocument",
161
convert_config: "ConvertCommandConfig", # noqa: ARG002
162
) -> str:
163
return create_safe_document_file_name(document.reserved_title)
164
165
@classmethod166
def add_import_arguments(cls, parser: argparse.ArgumentParser) -> None: # noqa: ARG003
167
"""
168
Override to contribute this Format's own CLI flags to `convert`169
(e.g. ExcelFormat adds --excel-parser, ReqIFFormat adds170
--reqif-profile/--reqif-enable-mid/--reqif-import-markup). A custom171
Format registered via the project config can add its own flags here172
without touching ConvertCommand/ConvertCommandConfig at all -- the173
only thing shared across formats is input_path/output_path/174
--input-format/--output-format/--config. Default: no extra flags.175
"""176
return177
178
@classmethod179
def build_import_options(cls, args: argparse.Namespace) -> object: # noqa: ARG003
180
"""
181
Pluck this Format's own fields off the parsed argparse.Namespace182
(populated by this same Format's add_import_arguments()) into a183
type-safe, format-specific options object, e.g. a @dataclass. The184
return type is `object` here since ConvertAction dispatches to an185
arbitrary Format by handle and can't know the concrete type -- but186
each override should return its own concrete dataclass, and its own187
import_file() should declare that same concrete type as its188
parameter, so the plucking + consuming stays type-checked within189
that Format's own module.190
"""191
raise NotImplementedError(f"{cls.__name__} does not support import.")
192
193
def import_file(self, *args: object, **kwargs: object) -> object:
194
raise NotImplementedError(
195
f"{self.__class__.__name__} does not support import."
196
)197
198
def import_folder(self, *args: object, **kwargs: object) -> object:
199
raise NotImplementedError(
200
f"{self.__class__.__name__} does not support import."
201
)