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.
@@ -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("}")]