fprime-cpp-codegen 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fprime_cpp_codegen/__init__.py +162 -0
- fprime_cpp_codegen/body.py +448 -0
- fprime_cpp_codegen/builder/__init__.py +75 -0
- fprime_cpp_codegen/builder/base.py +55 -0
- fprime_cpp_codegen/builder/coercion.py +97 -0
- fprime_cpp_codegen/builder/decoration.py +201 -0
- fprime_cpp_codegen/builder/definitions.py +381 -0
- fprime_cpp_codegen/builder/document.py +155 -0
- fprime_cpp_codegen/builder/scopes.py +466 -0
- fprime_cpp_codegen/comments.py +144 -0
- fprime_cpp_codegen/doc.py +371 -0
- fprime_cpp_codegen/errors.py +25 -0
- fprime_cpp_codegen/formatting.py +89 -0
- fprime_cpp_codegen/fprime.py +149 -0
- fprime_cpp_codegen/lines.py +323 -0
- fprime_cpp_codegen/output.py +121 -0
- fprime_cpp_codegen/py.typed +0 -0
- fprime_cpp_codegen/writer.py +787 -0
- fprime_cpp_codegen-0.1.0.dist-info/METADATA +347 -0
- fprime_cpp_codegen-0.1.0.dist-info/RECORD +23 -0
- fprime_cpp_codegen-0.1.0.dist-info/WHEEL +5 -0
- fprime_cpp_codegen-0.1.0.dist-info/licenses/LICENSE +201 -0
- fprime_cpp_codegen-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""The immutable line model and the line algebra built on top of it.
|
|
2
|
+
|
|
3
|
+
A generated C++ file is a ``list[Line]``. A :class:`Line` carries its indentation
|
|
4
|
+
separately from its text, so a continuation line can be aligned to an arbitrary
|
|
5
|
+
column of the line before it and a blank line renders empty rather than as trailing
|
|
6
|
+
whitespace.
|
|
7
|
+
|
|
8
|
+
Every function here is pure, and none of them know anything about C++.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import TypeVar
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"INDENT_INCREMENT",
|
|
20
|
+
"IndentMode",
|
|
21
|
+
"Line",
|
|
22
|
+
"add_blank_postfix",
|
|
23
|
+
"add_blank_prefix",
|
|
24
|
+
"add_postfix_line",
|
|
25
|
+
"add_prefix",
|
|
26
|
+
"add_prefix_and_suffix",
|
|
27
|
+
"add_prefix_indent",
|
|
28
|
+
"add_prefix_line",
|
|
29
|
+
"add_separators",
|
|
30
|
+
"add_suffix",
|
|
31
|
+
"blank",
|
|
32
|
+
"blank_separated",
|
|
33
|
+
"flatten",
|
|
34
|
+
"flatten_with_prefix_line",
|
|
35
|
+
"indent_lines",
|
|
36
|
+
"intersperse",
|
|
37
|
+
"intersperse_blank_lines",
|
|
38
|
+
"join",
|
|
39
|
+
"join_lists",
|
|
40
|
+
"line",
|
|
41
|
+
"lines",
|
|
42
|
+
"lines_opt",
|
|
43
|
+
"render",
|
|
44
|
+
"strip_margin",
|
|
45
|
+
"wrap_in_scope",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
_T = TypeVar("_T")
|
|
49
|
+
|
|
50
|
+
#: The number of spaces one level of indentation adds.
|
|
51
|
+
INDENT_INCREMENT = 2
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class IndentMode(Enum):
|
|
55
|
+
"""How :func:`join_lists` treats the tail of the second list of lines.
|
|
56
|
+
|
|
57
|
+
``INDENT`` re-indents the tail to line up under the join column, aligning a
|
|
58
|
+
doxygen post-comment beneath its parameter. ``NO_INDENT`` leaves the tail where
|
|
59
|
+
it is, for gluing a suffix such as ``;`` onto a multi-line signature.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
INDENT = "indent"
|
|
63
|
+
NO_INDENT = "no-indent"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class Line:
|
|
68
|
+
"""A single line of output: some text plus the column it starts at.
|
|
69
|
+
|
|
70
|
+
``indent`` is a raw space count and may go negative, which renders as no indent.
|
|
71
|
+
Access tags rely on that, being emitted at the class-body indent then shifted out
|
|
72
|
+
by two.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
string: str = ""
|
|
76
|
+
indent: int = 0
|
|
77
|
+
|
|
78
|
+
def __str__(self) -> str:
|
|
79
|
+
"""Render the line. An empty line renders empty, never as whitespace."""
|
|
80
|
+
if not self.string:
|
|
81
|
+
return ""
|
|
82
|
+
return " " * self.indent + self.string
|
|
83
|
+
|
|
84
|
+
def indent_in(self, n: int = INDENT_INCREMENT) -> Line:
|
|
85
|
+
"""Return this line indented in by ``n`` spaces."""
|
|
86
|
+
return Line(self.string, self.indent + n)
|
|
87
|
+
|
|
88
|
+
def indent_out(self, n: int = INDENT_INCREMENT) -> Line:
|
|
89
|
+
"""Return this line indented out by ``n`` spaces."""
|
|
90
|
+
return Line(self.string, self.indent - n)
|
|
91
|
+
|
|
92
|
+
def indent_to(self, n: int) -> Line:
|
|
93
|
+
"""Return this line at the absolute indent ``n``."""
|
|
94
|
+
return Line(self.string, n)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def size(self) -> int:
|
|
98
|
+
"""The rendered width of the line, indentation included, newline excluded.
|
|
99
|
+
|
|
100
|
+
The column :func:`join_lists` aligns against. Computed from the raw indent,
|
|
101
|
+
so a negative indent shrinks it.
|
|
102
|
+
"""
|
|
103
|
+
return self.indent + len(self.string)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def line(s: str) -> Line:
|
|
107
|
+
"""Construct a single unindented line."""
|
|
108
|
+
return Line(s)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def blank() -> Line:
|
|
112
|
+
"""Construct a blank line."""
|
|
113
|
+
return Line()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def strip_margin(s: str, margin: str = "|") -> str:
|
|
117
|
+
"""Strip a leading margin from every line of ``s``.
|
|
118
|
+
|
|
119
|
+
For each line, leading whitespace and control characters are skipped; if the next
|
|
120
|
+
character is ``margin``, it and everything before it are dropped. A line with no
|
|
121
|
+
margin marker is left untouched, including its leading whitespace -- so
|
|
122
|
+
``"x = a | b;"`` survives intact.
|
|
123
|
+
"""
|
|
124
|
+
out: list[str] = []
|
|
125
|
+
for part in s.split("\n"):
|
|
126
|
+
i = 0
|
|
127
|
+
n = len(part)
|
|
128
|
+
while i < n and part[i] <= " ":
|
|
129
|
+
i += 1
|
|
130
|
+
if i < n and part[i] == margin:
|
|
131
|
+
out.append(part[i + 1 :])
|
|
132
|
+
else:
|
|
133
|
+
out.append(part)
|
|
134
|
+
return "\n".join(out)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _split_lines(s: str) -> list[str]:
|
|
138
|
+
"""Split ``s`` on newlines, discarding trailing empty fields.
|
|
139
|
+
|
|
140
|
+
A string ending in a newline yields no spurious blank line, but the empty string
|
|
141
|
+
still yields one empty field. Interior blank lines are preserved.
|
|
142
|
+
"""
|
|
143
|
+
if s == "":
|
|
144
|
+
return [""]
|
|
145
|
+
parts = s.split("\n")
|
|
146
|
+
while parts and parts[-1] == "":
|
|
147
|
+
parts.pop()
|
|
148
|
+
return parts
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def lines(s: str) -> list[Line]:
|
|
152
|
+
"""Convert a (possibly margin-stripped, possibly multi-line) string to lines.
|
|
153
|
+
|
|
154
|
+
``lines("\\n|#ifndef X\\n|#define X")`` yields a leading blank line followed by the
|
|
155
|
+
two directives.
|
|
156
|
+
"""
|
|
157
|
+
return [Line(part) for part in _split_lines(strip_margin(s))]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def lines_opt(f: Callable[[_T], list[Line]], value: _T | None) -> list[Line]:
|
|
161
|
+
"""Apply ``f`` to ``value`` if it is not ``None``, else return no lines."""
|
|
162
|
+
return [] if value is None else f(value)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def indent_lines(ll: Iterable[Line], n: int = INDENT_INCREMENT) -> list[Line]:
|
|
166
|
+
"""Indent every line in ``ll`` in by ``n`` spaces."""
|
|
167
|
+
return [l.indent_in(n) for l in ll]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def join(sep: str, l1: Line, l2: Line) -> Line:
|
|
171
|
+
"""Concatenate two lines' text with ``sep``, keeping the first line's indent."""
|
|
172
|
+
return Line(l1.string + sep + l2.string, l1.indent)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def join_lists(
|
|
176
|
+
mode: IndentMode,
|
|
177
|
+
lines1: Sequence[Line],
|
|
178
|
+
sep: str,
|
|
179
|
+
lines2: Sequence[Line],
|
|
180
|
+
) -> list[Line]:
|
|
181
|
+
"""Glue two blocks of lines together at their seam.
|
|
182
|
+
|
|
183
|
+
The last line of ``lines1`` and the first of ``lines2`` merge into one line joined
|
|
184
|
+
by ``sep``. Under :attr:`IndentMode.INDENT` the remaining lines of ``lines2`` are
|
|
185
|
+
indented by the width of that seam, so they hang under the join column. Either
|
|
186
|
+
block being empty short-circuits to the other.
|
|
187
|
+
"""
|
|
188
|
+
if not lines2:
|
|
189
|
+
return list(lines1)
|
|
190
|
+
if not lines1:
|
|
191
|
+
return list(lines2)
|
|
192
|
+
head1, last1 = list(lines1[:-1]), lines1[-1]
|
|
193
|
+
first2, rest2 = lines2[0], list(lines2[1:])
|
|
194
|
+
joined = join(sep, last1, first2)
|
|
195
|
+
if mode is IndentMode.INDENT:
|
|
196
|
+
rest2 = indent_lines(rest2, last1.size + len(sep))
|
|
197
|
+
return head1 + [joined] + rest2
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def add_prefix(prefix: str, ll: Sequence[Line]) -> list[Line]:
|
|
201
|
+
"""Prepend ``prefix`` to the first line of ``ll``, without re-indenting."""
|
|
202
|
+
return join_lists(IndentMode.NO_INDENT, [Line(prefix)], "", ll)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def add_prefix_indent(prefix: str, ll: Sequence[Line]) -> list[Line]:
|
|
206
|
+
"""Prepend ``prefix`` to the first line of ``ll``, hanging the rest under it."""
|
|
207
|
+
return join_lists(IndentMode.INDENT, [Line(prefix)], "", ll)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def add_suffix(ll: Sequence[Line], suffix: str) -> list[Line]:
|
|
211
|
+
"""Append ``suffix`` to the last line of ``ll``."""
|
|
212
|
+
return join_lists(IndentMode.NO_INDENT, ll, "", [Line(suffix)])
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def add_prefix_and_suffix(prefix: str, ll: Sequence[Line], suffix: str) -> list[Line]:
|
|
216
|
+
"""Append ``suffix`` to the last line and prepend ``prefix`` to the first."""
|
|
217
|
+
return add_prefix(prefix, add_suffix(ll, suffix))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def add_prefix_line(prefix: Line, ll: Sequence[Line]) -> list[Line]:
|
|
221
|
+
"""Prepend ``prefix`` as its own line, but only if ``ll`` is non-empty."""
|
|
222
|
+
return [prefix, *ll] if ll else []
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def add_postfix_line(postfix: Line, ll: Sequence[Line]) -> list[Line]:
|
|
226
|
+
"""Append ``postfix`` as its own line, but only if ``ll`` is non-empty."""
|
|
227
|
+
return [*ll, postfix] if ll else []
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def add_blank_prefix(ll: Sequence[Line]) -> list[Line]:
|
|
231
|
+
"""Prepend a blank line, but only if ``ll`` is non-empty."""
|
|
232
|
+
return add_prefix_line(blank(), ll)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def add_blank_postfix(ll: Sequence[Line]) -> list[Line]:
|
|
236
|
+
"""Append a blank line, but only if ``ll`` is non-empty."""
|
|
237
|
+
return add_postfix_line(blank(), ll)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def flatten(sep: str, ll: Sequence[Line]) -> Line:
|
|
241
|
+
"""Collapse ``ll`` into a single line, joining the text with ``sep``."""
|
|
242
|
+
if not ll:
|
|
243
|
+
return blank()
|
|
244
|
+
result = ll[-1]
|
|
245
|
+
for l in reversed(ll[:-1]):
|
|
246
|
+
result = join(sep, l, result)
|
|
247
|
+
return result
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def flatten_with_prefix_line(prefix: Line, lll: Iterable[Sequence[Line]]) -> list[Line]:
|
|
251
|
+
"""Flatten a list of blocks, prefixing each non-empty block with ``prefix``."""
|
|
252
|
+
out: list[Line] = []
|
|
253
|
+
for ll in lll:
|
|
254
|
+
out.extend(add_prefix_line(prefix, ll))
|
|
255
|
+
return out
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def blank_separated(f: Callable[[_T], list[Line]], items: Sequence[_T]) -> list[Line]:
|
|
259
|
+
"""Map ``f`` over ``items`` and separate the results with single blank lines.
|
|
260
|
+
|
|
261
|
+
Empty results still contribute a separator; :func:`intersperse_blank_lines` drops
|
|
262
|
+
them.
|
|
263
|
+
"""
|
|
264
|
+
out: list[Line] = []
|
|
265
|
+
for i, item in enumerate(items):
|
|
266
|
+
if i:
|
|
267
|
+
out.append(blank())
|
|
268
|
+
out.extend(f(item))
|
|
269
|
+
return out
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def intersperse(items: Sequence[_T], element: _T) -> list[_T]:
|
|
273
|
+
"""Insert ``element`` between every pair of adjacent items."""
|
|
274
|
+
if len(items) <= 1:
|
|
275
|
+
return list(items)
|
|
276
|
+
out: list[_T] = [items[0]]
|
|
277
|
+
for item in items[1:]:
|
|
278
|
+
out.append(element)
|
|
279
|
+
out.append(item)
|
|
280
|
+
return out
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def intersperse_blank_lines(lll: Iterable[Sequence[Line]]) -> list[Line]:
|
|
284
|
+
"""Flatten a list of blocks with one blank line between them, dropping empty
|
|
285
|
+
blocks so they do not produce doubled blanks."""
|
|
286
|
+
blocks = [list(ll) for ll in lll if ll]
|
|
287
|
+
out: list[Line] = []
|
|
288
|
+
for i, block in enumerate(blocks):
|
|
289
|
+
if i:
|
|
290
|
+
out.append(blank())
|
|
291
|
+
out.extend(block)
|
|
292
|
+
return out
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def add_separators(sep: str, ll: Sequence[Line]) -> list[Line]:
|
|
296
|
+
"""Append ``sep`` to every line except the last. Useful for comma lists."""
|
|
297
|
+
last = len(ll) - 1
|
|
298
|
+
return [Line(l.string + sep, l.indent) if i < last else l for i, l in enumerate(ll)]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def wrap_in_scope(
|
|
302
|
+
opening: str,
|
|
303
|
+
body: Sequence[Line],
|
|
304
|
+
closing: str,
|
|
305
|
+
*,
|
|
306
|
+
keep_empty: bool = False,
|
|
307
|
+
) -> list[Line]:
|
|
308
|
+
"""Indent ``body`` one level between an ``opening`` and ``closing`` line.
|
|
309
|
+
|
|
310
|
+
An empty body yields nothing unless ``keep_empty`` is set, so a conditional block
|
|
311
|
+
with no content disappears instead of leaving empty braces.
|
|
312
|
+
"""
|
|
313
|
+
if not body and not keep_empty:
|
|
314
|
+
return []
|
|
315
|
+
return [*lines(opening), *indent_lines(body), *lines(closing)]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def render(ll: Iterable[Line]) -> str:
|
|
319
|
+
"""Render lines to file text, newline-separated and newline-terminated."""
|
|
320
|
+
items = list(ll)
|
|
321
|
+
if not items:
|
|
322
|
+
return ""
|
|
323
|
+
return "\n".join(str(l) for l in items) + "\n"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Turning a document into files on disk.
|
|
2
|
+
|
|
3
|
+
A file whose contents already match is left alone, keeping its mtime stable so a
|
|
4
|
+
no-op regeneration does not cascade a rebuild downstream. Pass
|
|
5
|
+
``skip_unchanged=False`` to always write.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .doc import Class, ClassMember, CppDoc, Member, Namespace
|
|
15
|
+
from .formatting import Formatter
|
|
16
|
+
from .writer import render_cpp, render_hpp
|
|
17
|
+
|
|
18
|
+
__all__ = ["WriteResult", "collect_cpp_files", "doc_files", "write_doc"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class WriteResult:
|
|
23
|
+
"""What :func:`write_doc` did."""
|
|
24
|
+
|
|
25
|
+
written: list[Path] = field(default_factory=list)
|
|
26
|
+
"""Files created or updated."""
|
|
27
|
+
|
|
28
|
+
unchanged: list[Path] = field(default_factory=list)
|
|
29
|
+
"""Files that already had the right contents and were left alone."""
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def all(self) -> list[Path]:
|
|
33
|
+
"""Every file the document owns, written or not, in generation order."""
|
|
34
|
+
return sorted([*self.written, *self.unchanged])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def collect_cpp_files(doc: CppDoc) -> list[str]:
|
|
38
|
+
"""Find every supplemental source file ``doc`` assigns definitions to.
|
|
39
|
+
|
|
40
|
+
Returns base names without extensions, in the order they first appear, and never
|
|
41
|
+
the document's own default file. Rendering uses this to discover its own
|
|
42
|
+
outputs; a file left unnamed would lose every definition assigned to it.
|
|
43
|
+
"""
|
|
44
|
+
default_base = doc.cpp_file_name.rsplit(".", 1)[0]
|
|
45
|
+
found: list[str] = []
|
|
46
|
+
|
|
47
|
+
def visit(members: Sequence[Member | ClassMember]) -> None:
|
|
48
|
+
for m in members:
|
|
49
|
+
base = getattr(m, "cpp_file", None)
|
|
50
|
+
if base is not None and base != default_base and base not in found:
|
|
51
|
+
found.append(base)
|
|
52
|
+
if isinstance(m, (Class, Namespace)):
|
|
53
|
+
visit(m.members)
|
|
54
|
+
|
|
55
|
+
visit(doc.members)
|
|
56
|
+
return found
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def doc_files(
|
|
60
|
+
doc: CppDoc,
|
|
61
|
+
cpp_files: Sequence[str] | None = None,
|
|
62
|
+
*,
|
|
63
|
+
formatter: Formatter | None = None,
|
|
64
|
+
) -> dict[str, str]:
|
|
65
|
+
"""Render a document to a mapping of file name to text.
|
|
66
|
+
|
|
67
|
+
Always produces the header and the document's default source file.
|
|
68
|
+
``cpp_files`` names additional source files by base name, without extension;
|
|
69
|
+
each gets only the definitions assigned to it. Left as ``None``, the
|
|
70
|
+
supplemental files are discovered from the document itself.
|
|
71
|
+
|
|
72
|
+
``formatter`` post-processes each file, receiving its text and its name; see
|
|
73
|
+
:mod:`fprime_cpp_codegen.formatting`.
|
|
74
|
+
"""
|
|
75
|
+
if cpp_files is None:
|
|
76
|
+
cpp_files = collect_cpp_files(doc)
|
|
77
|
+
out = {
|
|
78
|
+
doc.hpp_file.name: render_hpp(doc),
|
|
79
|
+
doc.cpp_file_name: render_cpp(doc),
|
|
80
|
+
}
|
|
81
|
+
for base in cpp_files:
|
|
82
|
+
out[f"{base}.cpp"] = render_cpp(doc, base)
|
|
83
|
+
if formatter is not None:
|
|
84
|
+
out = {name: formatter(text, name) for name, text in out.items()}
|
|
85
|
+
return out
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def write_doc(
|
|
89
|
+
doc: CppDoc,
|
|
90
|
+
directory: str | Path = ".",
|
|
91
|
+
cpp_files: Sequence[str] | None = None,
|
|
92
|
+
*,
|
|
93
|
+
formatter: Formatter | None = None,
|
|
94
|
+
skip_unchanged: bool = True,
|
|
95
|
+
encoding: str = "utf-8",
|
|
96
|
+
) -> WriteResult:
|
|
97
|
+
"""Write a document's header and source files into ``directory``.
|
|
98
|
+
|
|
99
|
+
The directory is created if it does not exist. See :func:`doc_files` for how
|
|
100
|
+
``cpp_files`` selects supplemental source files and what ``formatter`` does.
|
|
101
|
+
|
|
102
|
+
Formatting happens before the unchanged check, so a file already holding the
|
|
103
|
+
formatted text is still left alone.
|
|
104
|
+
"""
|
|
105
|
+
root = Path(directory)
|
|
106
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
written: list[Path] = []
|
|
108
|
+
unchanged: list[Path] = []
|
|
109
|
+
for name, text in doc_files(doc, cpp_files, formatter=formatter).items():
|
|
110
|
+
path = root / name
|
|
111
|
+
if (
|
|
112
|
+
skip_unchanged
|
|
113
|
+
and path.is_file()
|
|
114
|
+
and path.read_text(encoding=encoding) == text
|
|
115
|
+
):
|
|
116
|
+
unchanged.append(path)
|
|
117
|
+
continue
|
|
118
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
path.write_text(text, encoding=encoding)
|
|
120
|
+
written.append(path)
|
|
121
|
+
return WriteResult(written, unchanged)
|
|
File without changes
|