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,466 @@
|
|
|
1
|
+
"""Scopes: the things that hold an ordered list of members.
|
|
2
|
+
|
|
3
|
+
A class body, a namespace, and the document itself all share the member-adding
|
|
4
|
+
vocabulary in :class:`_Scope`, and differ in what a member may be.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Generator, Iterable, Sequence
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from typing import Any, Generic
|
|
12
|
+
|
|
13
|
+
from ..comments import (
|
|
14
|
+
write_banner_comment,
|
|
15
|
+
write_doxygen_comment,
|
|
16
|
+
write_doxygen_comment_opt,
|
|
17
|
+
)
|
|
18
|
+
from ..doc import Class, Lines, Namespace, Output, Type, Variable, as_type
|
|
19
|
+
from ..errors import ValidationError
|
|
20
|
+
from ..lines import Line, blank
|
|
21
|
+
from ..lines import line as _line
|
|
22
|
+
from ..lines import lines as _lines
|
|
23
|
+
from .base import _Builder, _DocContext, _resolve, _T, _T2
|
|
24
|
+
from .coercion import _extends
|
|
25
|
+
from .decoration import AccessSection, _Guard, _GuardClose, _GuardOpen
|
|
26
|
+
from .definitions import (
|
|
27
|
+
Radix,
|
|
28
|
+
ConstructorBuilder,
|
|
29
|
+
DestructorBuilder,
|
|
30
|
+
EnumBuilder,
|
|
31
|
+
FunctionBuilder,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _Scope(_Builder[_T], Generic[_T]):
|
|
36
|
+
"""Shared behaviour for anything that holds an ordered list of members."""
|
|
37
|
+
|
|
38
|
+
_in_class = False
|
|
39
|
+
"""Whether this scope is a class body. Decides how a variable's declaration
|
|
40
|
+
and definition are split."""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self, ctx: _DocContext | None = None, *, type_qualifier: str = ""
|
|
44
|
+
) -> None:
|
|
45
|
+
self._ctx = ctx if ctx is not None else _DocContext()
|
|
46
|
+
self._pending: list[object] = []
|
|
47
|
+
self._type_qualifier = type_qualifier
|
|
48
|
+
"""How a type declared in this scope is spelled from a source file: the
|
|
49
|
+
enclosing class chain, or empty at namespace scope."""
|
|
50
|
+
|
|
51
|
+
# -- internals ----------------------------------------------------
|
|
52
|
+
|
|
53
|
+
def _add(self, item: _T2) -> _T2:
|
|
54
|
+
self._pending.append(item)
|
|
55
|
+
return item
|
|
56
|
+
|
|
57
|
+
def _resolved_cpp_file(self, cpp_file: str | None) -> str | None:
|
|
58
|
+
"""Fall back to the enclosing ``cpp_file`` block when none was given."""
|
|
59
|
+
return cpp_file if cpp_file is not None else self._ctx.cpp_file
|
|
60
|
+
|
|
61
|
+
def _built_members(self) -> list[Any]:
|
|
62
|
+
return _resolve(self._pending)
|
|
63
|
+
|
|
64
|
+
# -- raw content --------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def member(self, *members: object) -> None:
|
|
67
|
+
"""Splice in ready-made IR members or builders, in order.
|
|
68
|
+
|
|
69
|
+
Takes output from :mod:`fprime_cpp_codegen.fprime`::
|
|
70
|
+
|
|
71
|
+
cls.member(*fprime.write_ostream_operator("MyType", body))
|
|
72
|
+
"""
|
|
73
|
+
for m in members:
|
|
74
|
+
self._add(m)
|
|
75
|
+
|
|
76
|
+
def raw(
|
|
77
|
+
self,
|
|
78
|
+
ll: Iterable[Line],
|
|
79
|
+
*,
|
|
80
|
+
output: Output = Output.HPP,
|
|
81
|
+
cpp_file: str | None = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Append already-rendered lines as a member."""
|
|
84
|
+
self._add(Lines(list(ll), output, self._resolved_cpp_file(cpp_file)))
|
|
85
|
+
|
|
86
|
+
def lines(
|
|
87
|
+
self,
|
|
88
|
+
text: str,
|
|
89
|
+
*,
|
|
90
|
+
output: Output = Output.HPP,
|
|
91
|
+
cpp_file: str | None = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Append a margin-stripped, possibly multi-line block of C++ as a member."""
|
|
94
|
+
self.raw(_lines(text), output=output, cpp_file=cpp_file)
|
|
95
|
+
|
|
96
|
+
def banner(
|
|
97
|
+
self,
|
|
98
|
+
text: str,
|
|
99
|
+
*,
|
|
100
|
+
output: Output = Output.BOTH,
|
|
101
|
+
cpp_file: str | None = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Append a ruled banner comment, heading the members that follow.
|
|
104
|
+
|
|
105
|
+
Unconditional: it goes into both files unless ``output`` says otherwise. An
|
|
106
|
+
access section's banner instead follows its members.
|
|
107
|
+
"""
|
|
108
|
+
self.raw(write_banner_comment(text), output=output, cpp_file=cpp_file)
|
|
109
|
+
|
|
110
|
+
def doc_comment(self, text: str, *, output: Output = Output.HPP) -> None:
|
|
111
|
+
"""Append a standalone ``//!`` doxygen comment."""
|
|
112
|
+
self.raw(write_doxygen_comment(text), output=output)
|
|
113
|
+
|
|
114
|
+
def using(
|
|
115
|
+
self,
|
|
116
|
+
name: str,
|
|
117
|
+
target: str,
|
|
118
|
+
*,
|
|
119
|
+
comment: str | None = None,
|
|
120
|
+
output: Output = Output.HPP,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Append a type alias: ``using <name> = <target>;``."""
|
|
123
|
+
self.raw(
|
|
124
|
+
[*write_doxygen_comment_opt(comment), *_lines(f"using {name} = {target};")],
|
|
125
|
+
output=output,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# -- nested constructs -------------------------------------------
|
|
129
|
+
|
|
130
|
+
def enum(
|
|
131
|
+
self,
|
|
132
|
+
name: str | None = None,
|
|
133
|
+
*,
|
|
134
|
+
comment: str | None = None,
|
|
135
|
+
output: Output = Output.HPP,
|
|
136
|
+
radix: Radix = Radix.DECIMAL,
|
|
137
|
+
trailing_comma: bool = True,
|
|
138
|
+
comment_above: bool = False,
|
|
139
|
+
) -> EnumBuilder:
|
|
140
|
+
"""Add an unscoped ``enum``, named or anonymous.
|
|
141
|
+
|
|
142
|
+
An anonymous enum carries an integer constant in the header without needing a
|
|
143
|
+
definition in a source file.
|
|
144
|
+
"""
|
|
145
|
+
return self._add(
|
|
146
|
+
EnumBuilder(
|
|
147
|
+
name,
|
|
148
|
+
comment=comment,
|
|
149
|
+
output=output,
|
|
150
|
+
radix=radix,
|
|
151
|
+
trailing_comma=trailing_comma,
|
|
152
|
+
comment_above=comment_above,
|
|
153
|
+
qualifier=self._type_qualifier,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def enum_class(
|
|
158
|
+
self,
|
|
159
|
+
name: str,
|
|
160
|
+
*,
|
|
161
|
+
underlying: str | None = None,
|
|
162
|
+
comment: str | None = None,
|
|
163
|
+
output: Output = Output.HPP,
|
|
164
|
+
radix: Radix = Radix.DECIMAL,
|
|
165
|
+
trailing_comma: bool = True,
|
|
166
|
+
comment_above: bool = False,
|
|
167
|
+
) -> EnumBuilder:
|
|
168
|
+
"""Add a scoped ``enum class``, optionally with an underlying type."""
|
|
169
|
+
return self._add(
|
|
170
|
+
EnumBuilder(
|
|
171
|
+
name,
|
|
172
|
+
underlying=underlying,
|
|
173
|
+
scoped=True,
|
|
174
|
+
comment=comment,
|
|
175
|
+
output=output,
|
|
176
|
+
radix=radix,
|
|
177
|
+
trailing_comma=trailing_comma,
|
|
178
|
+
comment_above=comment_above,
|
|
179
|
+
qualifier=self._type_qualifier,
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def var(
|
|
184
|
+
self,
|
|
185
|
+
type_name: Type | str,
|
|
186
|
+
name: str,
|
|
187
|
+
*,
|
|
188
|
+
init: str | None = None,
|
|
189
|
+
array: str | None = None,
|
|
190
|
+
comment: str | None = None,
|
|
191
|
+
static: bool = False,
|
|
192
|
+
const: bool = False,
|
|
193
|
+
constexpr: bool = False,
|
|
194
|
+
mutable: bool = False,
|
|
195
|
+
extern: bool = False,
|
|
196
|
+
out_of_line_definition: bool = False,
|
|
197
|
+
cpp_file: str | None = None,
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Add a variable: a class data member, or a constant at namespace scope."""
|
|
200
|
+
self._add(
|
|
201
|
+
Variable(
|
|
202
|
+
name,
|
|
203
|
+
as_type(type_name),
|
|
204
|
+
init=init,
|
|
205
|
+
array=array,
|
|
206
|
+
comment=comment,
|
|
207
|
+
static=static,
|
|
208
|
+
const=const,
|
|
209
|
+
constexpr=constexpr,
|
|
210
|
+
mutable=mutable,
|
|
211
|
+
extern=extern,
|
|
212
|
+
out_of_line_definition=out_of_line_definition,
|
|
213
|
+
cpp_file=self._resolved_cpp_file(cpp_file),
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
@contextmanager
|
|
218
|
+
def if_directive(
|
|
219
|
+
self, directive: str, *, output: Output = Output.BOTH
|
|
220
|
+
) -> Generator[Any]:
|
|
221
|
+
"""Bracket the members added inside with a preprocessor guard.
|
|
222
|
+
|
|
223
|
+
``directive`` is written verbatim and must include its ``#``. A guard with no
|
|
224
|
+
members inside it is not emitted, and a block that raises discards whatever
|
|
225
|
+
it added.
|
|
226
|
+
|
|
227
|
+
The guard is repeated into every source file receiving one of the guarded
|
|
228
|
+
definitions, so a definition sent to a supplemental ``.cpp`` stays guarded
|
|
229
|
+
there.
|
|
230
|
+
"""
|
|
231
|
+
start = len(self._pending)
|
|
232
|
+
try:
|
|
233
|
+
yield self
|
|
234
|
+
except BaseException:
|
|
235
|
+
del self._pending[start:]
|
|
236
|
+
raise
|
|
237
|
+
if len(self._pending) == start:
|
|
238
|
+
return
|
|
239
|
+
guard = _Guard(directive, output, in_class=self._in_class)
|
|
240
|
+
guard.members = self._pending[start:]
|
|
241
|
+
self._pending.insert(start, _GuardOpen(guard))
|
|
242
|
+
self._pending.append(_GuardClose(guard))
|
|
243
|
+
|
|
244
|
+
@contextmanager
|
|
245
|
+
def cpp_file(self, base: str | None) -> Generator[Any]:
|
|
246
|
+
"""Send definitions created inside this block to ``<base>.cpp``.
|
|
247
|
+
|
|
248
|
+
``base`` is a file name without extension; ``None`` restores the document
|
|
249
|
+
default. Only definitions created while the block is open are affected.
|
|
250
|
+
"""
|
|
251
|
+
self._ctx.cpp_files.append(base)
|
|
252
|
+
try:
|
|
253
|
+
yield self
|
|
254
|
+
finally:
|
|
255
|
+
self._ctx.cpp_files.pop()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class ClassBuilder(_Scope[Class]):
|
|
259
|
+
"""A class or struct under construction."""
|
|
260
|
+
|
|
261
|
+
_in_class = True
|
|
262
|
+
|
|
263
|
+
def __init__(
|
|
264
|
+
self,
|
|
265
|
+
name: str,
|
|
266
|
+
*,
|
|
267
|
+
extends: str | Sequence[str] | None = None,
|
|
268
|
+
final: bool = False,
|
|
269
|
+
comment: str | None = None,
|
|
270
|
+
template: str | None = None,
|
|
271
|
+
struct: bool = False,
|
|
272
|
+
ctx: _DocContext | None = None,
|
|
273
|
+
type_qualifier: str = "",
|
|
274
|
+
) -> None:
|
|
275
|
+
if not name:
|
|
276
|
+
raise ValidationError("a class needs a name")
|
|
277
|
+
qualified = f"{type_qualifier}::{name}" if type_qualifier else name
|
|
278
|
+
super().__init__(ctx, type_qualifier=qualified)
|
|
279
|
+
self.name = name
|
|
280
|
+
self.qualified_name = qualified
|
|
281
|
+
"""How this class is spelled from a source file: the enclosing class chain
|
|
282
|
+
plus its own name. Namespaces are excluded, since a source file is written
|
|
283
|
+
inside its namespace."""
|
|
284
|
+
|
|
285
|
+
self.extends = _extends(extends)
|
|
286
|
+
self.final = final
|
|
287
|
+
self.comment = comment
|
|
288
|
+
self.template = template
|
|
289
|
+
self.struct = struct
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def type(self) -> Type:
|
|
293
|
+
"""This class as a :class:`Type`, qualified for use in a source file."""
|
|
294
|
+
return Type(
|
|
295
|
+
self.name, self.qualified_name if self.qualified_name != self.name else None
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def nested(self, name: str) -> Type:
|
|
299
|
+
"""A type declared inside this class, qualified for use in a source file.
|
|
300
|
+
|
|
301
|
+
For anything this class declares that the builder does not know about, such as
|
|
302
|
+
an alias from :meth:`using`::
|
|
303
|
+
|
|
304
|
+
cls.using("Id", "U32")
|
|
305
|
+
cls.function("id", ret=cls.nested("Id"), const=True)
|
|
306
|
+
"""
|
|
307
|
+
return Type(name, f"{self.qualified_name}::{name}")
|
|
308
|
+
|
|
309
|
+
# -- access sections ---------------------------------------------
|
|
310
|
+
|
|
311
|
+
def public(self, comment: str | None = None) -> AccessSection:
|
|
312
|
+
"""Start a ``public:`` section."""
|
|
313
|
+
return AccessSection(self, "public", comment)
|
|
314
|
+
|
|
315
|
+
def protected(self, comment: str | None = None) -> AccessSection:
|
|
316
|
+
"""Start a ``protected:`` section."""
|
|
317
|
+
return AccessSection(self, "protected", comment)
|
|
318
|
+
|
|
319
|
+
def private(self, comment: str | None = None) -> AccessSection:
|
|
320
|
+
"""Start a ``private:`` section."""
|
|
321
|
+
return AccessSection(self, "private", comment)
|
|
322
|
+
|
|
323
|
+
# -- members ------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
def constructor(self, **kwargs: Any) -> ConstructorBuilder:
|
|
326
|
+
"""Add a constructor. See :class:`ConstructorBuilder` for the arguments."""
|
|
327
|
+
kwargs.setdefault("cpp_file", self._ctx.cpp_file)
|
|
328
|
+
return self._add(ConstructorBuilder(**kwargs))
|
|
329
|
+
|
|
330
|
+
def destructor(self, **kwargs: Any) -> DestructorBuilder:
|
|
331
|
+
"""Add a destructor. See :class:`DestructorBuilder` for the arguments."""
|
|
332
|
+
kwargs.setdefault("cpp_file", self._ctx.cpp_file)
|
|
333
|
+
return self._add(DestructorBuilder(**kwargs))
|
|
334
|
+
|
|
335
|
+
def function(self, name: str, **kwargs: Any) -> FunctionBuilder:
|
|
336
|
+
"""Add a member function. See :class:`FunctionBuilder` for the arguments."""
|
|
337
|
+
kwargs.setdefault("cpp_file", self._ctx.cpp_file)
|
|
338
|
+
return self._add(FunctionBuilder(name, **kwargs))
|
|
339
|
+
|
|
340
|
+
def class_(self, name: str, **kwargs: Any) -> ClassBuilder:
|
|
341
|
+
"""Add a nested class."""
|
|
342
|
+
kwargs.setdefault("ctx", self._ctx)
|
|
343
|
+
kwargs.setdefault("type_qualifier", self._type_qualifier)
|
|
344
|
+
return self._add(ClassBuilder(name, **kwargs))
|
|
345
|
+
|
|
346
|
+
def struct_(self, name: str, **kwargs: Any) -> ClassBuilder:
|
|
347
|
+
"""Add a nested struct."""
|
|
348
|
+
kwargs["struct"] = True
|
|
349
|
+
return self.class_(name, **kwargs)
|
|
350
|
+
|
|
351
|
+
def friend(self, declaration: str, *, comment: str | None = None) -> None:
|
|
352
|
+
"""Add a ``friend`` declaration, written verbatim after the keyword."""
|
|
353
|
+
self.raw(
|
|
354
|
+
[
|
|
355
|
+
*write_doxygen_comment_opt(comment),
|
|
356
|
+
*_lines(f"friend {declaration};"),
|
|
357
|
+
]
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
def build(self) -> Class:
|
|
361
|
+
return Class(
|
|
362
|
+
self.name,
|
|
363
|
+
superclass_decls=self.extends,
|
|
364
|
+
members=self._built_members(),
|
|
365
|
+
comment=self.comment,
|
|
366
|
+
final=self.final,
|
|
367
|
+
template=self.template,
|
|
368
|
+
struct=self.struct,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def __enter__(self) -> ClassBuilder:
|
|
372
|
+
return self
|
|
373
|
+
|
|
374
|
+
def __exit__(self, *exc: object) -> None:
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class _MemberScope(_Scope[_T], Generic[_T]):
|
|
379
|
+
"""Document and namespace scope: classes, free functions, nested namespaces."""
|
|
380
|
+
|
|
381
|
+
def include(
|
|
382
|
+
self,
|
|
383
|
+
*paths: str,
|
|
384
|
+
output: Output = Output.HPP,
|
|
385
|
+
cpp_file: str | None = None,
|
|
386
|
+
) -> None:
|
|
387
|
+
"""Append quoted ``#include`` directives for project headers."""
|
|
388
|
+
if not paths:
|
|
389
|
+
return
|
|
390
|
+
self.raw(
|
|
391
|
+
[blank(), *(_line(f'#include "{p}"') for p in paths)],
|
|
392
|
+
output=output,
|
|
393
|
+
cpp_file=cpp_file,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def system_include(
|
|
397
|
+
self,
|
|
398
|
+
*paths: str,
|
|
399
|
+
output: Output = Output.HPP,
|
|
400
|
+
cpp_file: str | None = None,
|
|
401
|
+
) -> None:
|
|
402
|
+
"""Append angle-bracket ``#include`` directives for system headers."""
|
|
403
|
+
if not paths:
|
|
404
|
+
return
|
|
405
|
+
self.raw(
|
|
406
|
+
[blank(), *(_line(f"#include <{p}>") for p in paths)],
|
|
407
|
+
output=output,
|
|
408
|
+
cpp_file=cpp_file,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
def class_(self, name: str, **kwargs: Any) -> ClassBuilder:
|
|
412
|
+
"""Add a class."""
|
|
413
|
+
kwargs.setdefault("ctx", self._ctx)
|
|
414
|
+
return self._add(ClassBuilder(name, **kwargs))
|
|
415
|
+
|
|
416
|
+
def struct_(self, name: str, **kwargs: Any) -> ClassBuilder:
|
|
417
|
+
"""Add a struct."""
|
|
418
|
+
kwargs["struct"] = True
|
|
419
|
+
return self.class_(name, **kwargs)
|
|
420
|
+
|
|
421
|
+
def function(self, name: str, **kwargs: Any) -> FunctionBuilder:
|
|
422
|
+
"""Add a free function. The class-only qualifiers are rejected here."""
|
|
423
|
+
for bad in ("const", "virtual", "pure_virtual", "override", "final"):
|
|
424
|
+
if kwargs.get(bad):
|
|
425
|
+
raise ValidationError(
|
|
426
|
+
f"{bad!r} only means something for a member function; "
|
|
427
|
+
f"{name!r} is at namespace scope"
|
|
428
|
+
)
|
|
429
|
+
kwargs.setdefault("cpp_file", self._ctx.cpp_file)
|
|
430
|
+
return self._add(FunctionBuilder(name, **kwargs))
|
|
431
|
+
|
|
432
|
+
def namespace(self, *names: str) -> NamespaceBuilder:
|
|
433
|
+
"""Add a namespace, or a chain of nested ones.
|
|
434
|
+
|
|
435
|
+
``namespace("Fw", "Cfg")`` opens both and returns the innermost, so members
|
|
436
|
+
added to the result land in ``Fw::Cfg``.
|
|
437
|
+
"""
|
|
438
|
+
if not names:
|
|
439
|
+
raise ValidationError("namespace() needs at least one name")
|
|
440
|
+
outer = NamespaceBuilder(names[0], ctx=self._ctx)
|
|
441
|
+
self._add(outer)
|
|
442
|
+
inner = outer
|
|
443
|
+
for name in names[1:]:
|
|
444
|
+
inner = inner._add(NamespaceBuilder(name, ctx=self._ctx))
|
|
445
|
+
return inner
|
|
446
|
+
|
|
447
|
+
def anonymous_namespace(self) -> NamespaceBuilder:
|
|
448
|
+
"""Add an unnamed namespace, giving everything in it internal linkage."""
|
|
449
|
+
return self._add(NamespaceBuilder("", ctx=self._ctx))
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
class NamespaceBuilder(_MemberScope[Namespace]):
|
|
453
|
+
"""A namespace under construction."""
|
|
454
|
+
|
|
455
|
+
def __init__(self, name: str, *, ctx: _DocContext | None = None) -> None:
|
|
456
|
+
super().__init__(ctx)
|
|
457
|
+
self.name = name
|
|
458
|
+
|
|
459
|
+
def build(self) -> Namespace:
|
|
460
|
+
return Namespace(self.name, self._built_members())
|
|
461
|
+
|
|
462
|
+
def __enter__(self) -> NamespaceBuilder:
|
|
463
|
+
return self
|
|
464
|
+
|
|
465
|
+
def __exit__(self, *exc: object) -> None:
|
|
466
|
+
return None
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Comment, banner, and access-tag rendering.
|
|
2
|
+
|
|
3
|
+
Doxygen ``//!`` comments above declarations, ``//!<`` post comments hanging off
|
|
4
|
+
parameters, and ruled banners separating sections.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .doc import FileBanner
|
|
10
|
+
from .lines import (
|
|
11
|
+
INDENT_INCREMENT,
|
|
12
|
+
IndentMode,
|
|
13
|
+
Line,
|
|
14
|
+
blank,
|
|
15
|
+
indent_lines,
|
|
16
|
+
join,
|
|
17
|
+
join_lists,
|
|
18
|
+
line,
|
|
19
|
+
lines,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"BANNER_RULE",
|
|
24
|
+
"add_comment_prefix",
|
|
25
|
+
"add_param_comment",
|
|
26
|
+
"left_align_directive",
|
|
27
|
+
"write_access_tag",
|
|
28
|
+
"write_banner",
|
|
29
|
+
"write_banner_comment",
|
|
30
|
+
"write_comment",
|
|
31
|
+
"write_comment_body",
|
|
32
|
+
"write_doxygen_comment",
|
|
33
|
+
"write_doxygen_comment_opt",
|
|
34
|
+
"write_doxygen_post_comment",
|
|
35
|
+
"write_doxygen_post_comment_opt",
|
|
36
|
+
"write_function_body",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
#: The horizontal rule that delimits a banner comment.
|
|
40
|
+
BANNER_RULE = (
|
|
41
|
+
"// ----------------------------------------------------------------------"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def add_comment_prefix(prefix: str, l: Line) -> Line:
|
|
46
|
+
"""Prefix a comment line.
|
|
47
|
+
|
|
48
|
+
A blank line inside a multi-line comment becomes a bare ``//!``, with no trailing
|
|
49
|
+
space.
|
|
50
|
+
"""
|
|
51
|
+
if not l.string:
|
|
52
|
+
return line(prefix)
|
|
53
|
+
return join(" ", line(prefix), l)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def write_comment_body(comment: str) -> list[Line]:
|
|
57
|
+
"""Render ``comment`` as ``//`` lines, with no leading blank."""
|
|
58
|
+
return [add_comment_prefix("//", l) for l in lines(comment)]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def write_comment(comment: str) -> list[Line]:
|
|
62
|
+
"""Render ``comment`` as ``//`` lines, preceded by a blank line."""
|
|
63
|
+
return [blank(), *write_comment_body(comment)]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def write_banner_comment(comment: str) -> list[Line]:
|
|
67
|
+
"""Render ``comment`` as a ruled banner, preceded by a blank line."""
|
|
68
|
+
rule = line(BANNER_RULE)
|
|
69
|
+
return [blank(), rule, *write_comment_body(comment), rule]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def write_doxygen_comment(comment: str) -> list[Line]:
|
|
73
|
+
"""Render ``comment`` as ``//!`` lines, preceded by a blank line."""
|
|
74
|
+
return [blank(), *(add_comment_prefix("//!", l) for l in lines(comment))]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def write_doxygen_comment_opt(comment: str | None) -> list[Line]:
|
|
78
|
+
"""Render an optional doxygen comment.
|
|
79
|
+
|
|
80
|
+
``None`` yields a single blank line, keeping declarations separated whether or not
|
|
81
|
+
they are documented.
|
|
82
|
+
"""
|
|
83
|
+
return write_doxygen_comment(comment) if comment is not None else [blank()]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def write_doxygen_post_comment(comment: str) -> list[Line]:
|
|
87
|
+
"""Render ``comment`` as ``//!<`` lines, with no leading blank."""
|
|
88
|
+
return [add_comment_prefix("//!<", l) for l in lines(comment)]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def write_doxygen_post_comment_opt(comment: str | None) -> list[Line]:
|
|
92
|
+
"""Render an optional doxygen post comment, or a single blank line."""
|
|
93
|
+
return write_doxygen_post_comment(comment) if comment is not None else [blank()]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def add_param_comment(s: str, comment: str | None) -> list[Line]:
|
|
97
|
+
"""Hang a doxygen post-comment off the end of ``s``.
|
|
98
|
+
|
|
99
|
+
Continuation lines are indented to the column the comment starts at, stacking
|
|
100
|
+
under its first line. Used for parameters and enumerated constants.
|
|
101
|
+
"""
|
|
102
|
+
if comment is None:
|
|
103
|
+
return lines(s)
|
|
104
|
+
return join_lists(
|
|
105
|
+
IndentMode.INDENT, lines(s), " ", write_doxygen_post_comment(comment)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def write_access_tag(tag: str) -> list[Line]:
|
|
110
|
+
"""Render an access-specifier label such as ``public:``.
|
|
111
|
+
|
|
112
|
+
Shifted out by two spaces to sit half a level left of the members it governs,
|
|
113
|
+
which are indented two levels into the class body.
|
|
114
|
+
"""
|
|
115
|
+
return [blank(), line(f"{tag}:").indent_out(2)]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def left_align_directive(l: Line) -> Line:
|
|
119
|
+
"""Force a preprocessor directive to column zero by discarding its indentation."""
|
|
120
|
+
return Line(l.string) if l.string.startswith("#") else l
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def write_banner(
|
|
124
|
+
banner: FileBanner,
|
|
125
|
+
file_name: str,
|
|
126
|
+
generic_description: str,
|
|
127
|
+
) -> list[Line]:
|
|
128
|
+
"""Render the ``\\title``/``\\author``/``\\brief`` block atop a file."""
|
|
129
|
+
return lines(
|
|
130
|
+
f"""|// ======================================================================
|
|
131
|
+
|// \\title {banner.title(file_name)}
|
|
132
|
+
|// \\author {banner.author(file_name)}
|
|
133
|
+
|// \\brief {banner.description(file_name, generic_description)}
|
|
134
|
+
|// ======================================================================"""
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def write_function_body(body: list[Line]) -> list[Line]:
|
|
139
|
+
"""Wrap ``body`` in braces, indenting it one level.
|
|
140
|
+
|
|
141
|
+
An empty body renders as braces around a single blank line.
|
|
142
|
+
"""
|
|
143
|
+
inner = indent_lines(body, INDENT_INCREMENT) if body else [blank()]
|
|
144
|
+
return [line("{"), *inner, line("}")]
|