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,381 @@
1
+ """Builders for the things a scope holds: functions, constructors, enums."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Sequence
6
+ from enum import Enum
7
+
8
+ from ..body import Body, Code
9
+ from ..comments import (
10
+ add_param_comment,
11
+ write_doxygen_comment,
12
+ write_doxygen_comment_opt,
13
+ )
14
+ from ..doc import Constructor, Destructor, Function, Lines, Output, Param, Type, as_type
15
+ from ..errors import ValidationError
16
+ from ..lines import Line, wrap_in_scope
17
+ from ..lines import line as _line
18
+ from .base import _Builder
19
+ from .coercion import _as_body_lines, _as_params, _sv_qualifier
20
+
21
+
22
+ class Radix(Enum):
23
+ """How to spell an integer literal."""
24
+
25
+ DECIMAL = "decimal"
26
+ HEX = "hex"
27
+
28
+
29
+ def _wrap_in_enum(
30
+ body: list[Line],
31
+ *,
32
+ name: str | None = None,
33
+ scoped: bool = False,
34
+ underlying: str | None = None,
35
+ ) -> list[Line]:
36
+ """Wrap enumerators in an ``enum``, ``enum <name>`` or ``enum class <name>``."""
37
+ if scoped:
38
+ suffix = f" : {underlying}" if underlying is not None else ""
39
+ opening = f"enum class {name}{suffix} {{"
40
+ elif name is not None:
41
+ opening = f"enum {name} {{"
42
+ else:
43
+ opening = "enum {"
44
+ return wrap_in_scope(opening, body, "};", keep_empty=True)
45
+
46
+
47
+ class FunctionBuilder(_Builder[Function]):
48
+ """A function or member function under construction."""
49
+
50
+ def __init__(
51
+ self,
52
+ name: str,
53
+ *,
54
+ ret: Type | str = "void",
55
+ params: Iterable[Param | Sequence[str]] = (),
56
+ comment: str | None = None,
57
+ body: Code = None,
58
+ const: bool = False,
59
+ static: bool = False,
60
+ virtual: bool = False,
61
+ pure_virtual: bool = False,
62
+ override: bool = False,
63
+ final: bool = False,
64
+ constexpr: bool = False,
65
+ inline: bool = False,
66
+ noexcept: bool = False,
67
+ deleted: bool = False,
68
+ defaulted: bool = False,
69
+ template: str | None = None,
70
+ inline_body: bool = False,
71
+ cpp_file: str | None = None,
72
+ ) -> None:
73
+ if not name:
74
+ raise ValidationError("a function needs a name")
75
+ self.name = name
76
+ self.ret = as_type(ret)
77
+ self.comment = comment
78
+ self.const = const
79
+ self.constexpr = constexpr
80
+ self.inline = inline
81
+ self.noexcept = noexcept
82
+ self.deleted = deleted
83
+ self.defaulted = defaulted
84
+ self.template = template
85
+ self.inline_body = inline_body
86
+ self.cpp_file = cpp_file
87
+ self._sv = _sv_qualifier(
88
+ static=static,
89
+ virtual=virtual,
90
+ pure_virtual=pure_virtual,
91
+ override=override,
92
+ final=final,
93
+ )
94
+ self._params = _as_params(params)
95
+ self.body = Body(_as_body_lines(body))
96
+
97
+ def param(
98
+ self,
99
+ type_name: Type | str,
100
+ name: str,
101
+ *,
102
+ comment: str | None = None,
103
+ default: str | None = None,
104
+ ) -> FunctionBuilder:
105
+ """Append one formal parameter. Returns self, so calls can be chained."""
106
+ self._params.append(Param(as_type(type_name), name, comment, default))
107
+ return self
108
+
109
+ def params(self, *params: Param | Sequence[str]) -> FunctionBuilder:
110
+ """Append several formal parameters at once."""
111
+ self._params.extend(_as_params(params))
112
+ return self
113
+
114
+ def build(self) -> Function:
115
+ return Function(
116
+ self.name,
117
+ params=list(self._params),
118
+ ret_type=self.ret,
119
+ body=self.body.build(),
120
+ comment=self.comment,
121
+ sv=self._sv,
122
+ const=self.const,
123
+ constexpr=self.constexpr,
124
+ inline=self.inline,
125
+ noexcept=self.noexcept,
126
+ deleted=self.deleted,
127
+ defaulted=self.defaulted,
128
+ template=self.template,
129
+ inline_body=self.inline_body,
130
+ cpp_file=self.cpp_file,
131
+ )
132
+
133
+ def __enter__(self) -> FunctionBuilder:
134
+ return self
135
+
136
+ def __exit__(self, *exc: object) -> None:
137
+ return None
138
+
139
+
140
+ class ConstructorBuilder(_Builder[Constructor]):
141
+ """A constructor under construction."""
142
+
143
+ def __init__(
144
+ self,
145
+ *,
146
+ params: Iterable[Param | Sequence[str]] = (),
147
+ initializers: Iterable[str] = (),
148
+ comment: str | None = None,
149
+ body: Code = None,
150
+ explicit: bool = False,
151
+ constexpr: bool = False,
152
+ noexcept: bool = False,
153
+ deleted: bool = False,
154
+ defaulted: bool = False,
155
+ template: str | None = None,
156
+ inline_body: bool = False,
157
+ cpp_file: str | None = None,
158
+ ) -> None:
159
+ self.comment = comment
160
+ self.explicit = explicit
161
+ self.constexpr = constexpr
162
+ self.noexcept = noexcept
163
+ self.deleted = deleted
164
+ self.defaulted = defaulted
165
+ self.template = template
166
+ self.inline_body = inline_body
167
+ self.cpp_file = cpp_file
168
+ self._params = _as_params(params)
169
+ self._initializers = list(initializers)
170
+ self.body = Body(_as_body_lines(body))
171
+
172
+ def param(
173
+ self,
174
+ type_name: Type | str,
175
+ name: str,
176
+ *,
177
+ comment: str | None = None,
178
+ default: str | None = None,
179
+ ) -> ConstructorBuilder:
180
+ """Append one formal parameter."""
181
+ self._params.append(Param(as_type(type_name), name, comment, default))
182
+ return self
183
+
184
+ def params(self, *params: Param | Sequence[str]) -> ConstructorBuilder:
185
+ """Append several formal parameters at once."""
186
+ self._params.extend(_as_params(params))
187
+ return self
188
+
189
+ def init(self, *entries: str) -> ConstructorBuilder:
190
+ """Append member-initializer entries, e.g. ``init("m_size(size)")``."""
191
+ self._initializers.extend(entries)
192
+ return self
193
+
194
+ def build(self) -> Constructor:
195
+ return Constructor(
196
+ params=list(self._params),
197
+ initializers=list(self._initializers),
198
+ body=self.body.build(),
199
+ comment=self.comment,
200
+ explicit=self.explicit,
201
+ constexpr=self.constexpr,
202
+ noexcept=self.noexcept,
203
+ deleted=self.deleted,
204
+ defaulted=self.defaulted,
205
+ template=self.template,
206
+ inline_body=self.inline_body,
207
+ cpp_file=self.cpp_file,
208
+ )
209
+
210
+ def __enter__(self) -> ConstructorBuilder:
211
+ return self
212
+
213
+ def __exit__(self, *exc: object) -> None:
214
+ return None
215
+
216
+
217
+ class DestructorBuilder(_Builder[Destructor]):
218
+ """A destructor under construction."""
219
+
220
+ def __init__(
221
+ self,
222
+ *,
223
+ comment: str | None = None,
224
+ body: Code = None,
225
+ virtual: bool = False,
226
+ override: bool = False,
227
+ noexcept: bool = False,
228
+ deleted: bool = False,
229
+ defaulted: bool = False,
230
+ inline_body: bool = False,
231
+ cpp_file: str | None = None,
232
+ ) -> None:
233
+ self.comment = comment
234
+ self.virtual = virtual
235
+ self.override = override
236
+ self.noexcept = noexcept
237
+ self.deleted = deleted
238
+ self.defaulted = defaulted
239
+ self.inline_body = inline_body
240
+ self.cpp_file = cpp_file
241
+ self.body = Body(_as_body_lines(body))
242
+
243
+ def build(self) -> Destructor:
244
+ return Destructor(
245
+ body=self.body.build(),
246
+ comment=self.comment,
247
+ virtual=self.virtual,
248
+ override=self.override,
249
+ noexcept=self.noexcept,
250
+ deleted=self.deleted,
251
+ defaulted=self.defaulted,
252
+ inline_body=self.inline_body,
253
+ cpp_file=self.cpp_file,
254
+ )
255
+
256
+ def __enter__(self) -> DestructorBuilder:
257
+ return self
258
+
259
+ def __exit__(self, *exc: object) -> None:
260
+ return None
261
+
262
+
263
+ class EnumBuilder(_Builder[Lines]):
264
+ """An enum or enum class under construction. Renders as raw lines."""
265
+
266
+ def __init__(
267
+ self,
268
+ name: str | None = None,
269
+ *,
270
+ underlying: str | None = None,
271
+ scoped: bool = False,
272
+ comment: str | None = None,
273
+ output: Output = Output.HPP,
274
+ cpp_file: str | None = None,
275
+ radix: Radix = Radix.DECIMAL,
276
+ trailing_comma: bool = True,
277
+ comment_above: bool = False,
278
+ qualifier: str = "",
279
+ ) -> None:
280
+ if underlying is not None and not scoped:
281
+ raise ValidationError(
282
+ "an underlying type needs a scoped enum; pass scoped=True (or use "
283
+ "enum_class())"
284
+ )
285
+ if scoped and not name:
286
+ raise ValidationError("a scoped enum needs a name")
287
+ self.name = name
288
+ self.underlying = underlying
289
+ self.scoped = scoped
290
+ self.comment = comment
291
+ self.output = output
292
+ self.cpp_file = cpp_file
293
+ self.radix = radix
294
+ self.trailing_comma = trailing_comma
295
+ """Whether the last enumerator carries a comma. Legal either way."""
296
+
297
+ self.comment_above = comment_above
298
+ """Put each enumerator's comment on a ``//!`` line above it, instead of a
299
+ ``//!<`` post-comment after it."""
300
+
301
+ self.qualifier = qualifier
302
+ self._constants: list[list[Line]] = []
303
+
304
+ @property
305
+ def type(self) -> Type:
306
+ """This enum as a :class:`Type`, qualified for use in a source file.
307
+
308
+ A nested enum is spelled bare inside its class, but a source-file return type
309
+ precedes ``Class::`` and so is not yet in the class's scope. Pass this to
310
+ ``ret=`` for both spellings::
311
+
312
+ status = cls.enum_class("Status", underlying="U8")
313
+ cls.function("check", ret=status.type)
314
+ """
315
+ if self.name is None:
316
+ raise ValidationError("an anonymous enum has no type name")
317
+ return Type(
318
+ self.name, f"{self.qualifier}::{self.name}" if self.qualifier else None
319
+ )
320
+
321
+ def constant(
322
+ self,
323
+ name: str,
324
+ value: int | str | None = None,
325
+ *,
326
+ comment: str | None = None,
327
+ radix: Radix | None = None,
328
+ ) -> EnumBuilder:
329
+ """Append one enumerator.
330
+
331
+ ``value`` may be an integer, an arbitrary C++ expression, or ``None`` to let
332
+ the compiler assign the next value.
333
+ """
334
+ if value is None:
335
+ text = f"{name},"
336
+ elif isinstance(value, int):
337
+ spelled = (
338
+ f"0x{value:x}" if (radix or self.radix) is Radix.HEX else str(value)
339
+ )
340
+ text = f"{name} = {spelled},"
341
+ else:
342
+ text = f"{name} = {value},"
343
+ if comment is not None and self.comment_above:
344
+ entry = [*write_doxygen_comment(comment)[1:], _line(text)]
345
+ else:
346
+ entry = add_param_comment(text, comment)
347
+ self._constants.append(entry)
348
+ return self
349
+
350
+ def constants(self, *names: str) -> EnumBuilder:
351
+ """Append several auto-numbered enumerators at once."""
352
+ for name in names:
353
+ self.constant(name)
354
+ return self
355
+
356
+ def _body(self) -> list[Line]:
357
+ """The enumerators, with the last one's comma removed if asked."""
358
+ entries = [list(e) for e in self._constants]
359
+ if entries and not self.trailing_comma:
360
+ last = entries[-1]
361
+ last[-1] = Line(last[-1].string.rstrip(","), last[-1].indent)
362
+ return [l for entry in entries for l in entry]
363
+
364
+ def build(self) -> Lines:
365
+ inner = _wrap_in_enum(
366
+ self._body(),
367
+ name=self.name,
368
+ scoped=self.scoped,
369
+ underlying=self.underlying,
370
+ )
371
+ return Lines(
372
+ [*write_doxygen_comment_opt(self.comment), *inner],
373
+ self.output,
374
+ self.cpp_file,
375
+ )
376
+
377
+ def __enter__(self) -> EnumBuilder:
378
+ return self
379
+
380
+ def __exit__(self, *exc: object) -> None:
381
+ return None
@@ -0,0 +1,155 @@
1
+ """The document scope: one header plus one or more source files, and their output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Sequence
7
+ from pathlib import Path
8
+
9
+ from ..doc import CppDoc, FileBanner, HppFile
10
+ from ..errors import ValidationError
11
+ from ..formatting import Formatter
12
+ from ..output import WriteResult, doc_files, write_doc
13
+ from ..writer import render_cpp, render_hpp
14
+ from .base import _DocContext
15
+ from .scopes import _MemberScope
16
+
17
+
18
+ class CppDocBuilder(_MemberScope[CppDoc]):
19
+ """A whole C++ document: one header and one or more source files."""
20
+
21
+ def __init__(
22
+ self,
23
+ file_base: str,
24
+ *,
25
+ description: str | None = None,
26
+ include_guard: str | None = None,
27
+ namespaces: Sequence[str] = (),
28
+ tool_name: str | None = None,
29
+ file_banner: FileBanner | None = None,
30
+ formatter: Formatter | None = None,
31
+ hpp_extension: str = "hpp",
32
+ cpp_extension: str = "cpp",
33
+ ) -> None:
34
+ """Start a document whose files are named after ``file_base``.
35
+
36
+ ``include_guard`` defaults to one derived from ``file_base`` and
37
+ ``namespaces``; ``namespaces`` is used for nothing else, so pass it when you
38
+ want ``Fw_Cfg_MyClass_HPP`` without spelling the macro out.
39
+
40
+ ``formatter`` post-processes every file this document renders; see
41
+ :mod:`fprime_cpp_codegen.formatting`. Any render or write call can override
42
+ it, but cannot switch it off.
43
+ """
44
+ super().__init__(_DocContext())
45
+ if not file_base:
46
+ raise ValidationError("a document needs a file name base")
47
+ self.file_base = file_base
48
+ self.description = description if description is not None else file_base
49
+ self.hpp_extension = hpp_extension
50
+ self.cpp_extension = cpp_extension
51
+ self.tool_name = tool_name
52
+ self.formatter = formatter
53
+ """Applied to every file this document renders, unless a call overrides it."""
54
+
55
+ self.file_banner = file_banner
56
+ """Overrides the ``\\title``/``\\author``/``\\brief`` block atop each file.
57
+ Distinct from :meth:`banner`, which emits a section comment."""
58
+
59
+ self.include_guard = (
60
+ include_guard
61
+ if include_guard is not None
62
+ else _default_guard(file_base, namespaces, hpp_extension)
63
+ )
64
+
65
+ @property
66
+ def hpp_name(self) -> str:
67
+ """The header file name, e.g. ``"MyClass.hpp"``."""
68
+ return f"{self.file_base}.{self.hpp_extension}"
69
+
70
+ @property
71
+ def cpp_name(self) -> str:
72
+ """The default source file name, e.g. ``"MyClass.cpp"``."""
73
+ return f"{self.file_base}.{self.cpp_extension}"
74
+
75
+ def build(self) -> CppDoc:
76
+ return CppDoc(
77
+ description=self.description,
78
+ hpp_file=HppFile(self.hpp_name, self.include_guard),
79
+ cpp_file_name=self.cpp_name,
80
+ members=self._built_members(),
81
+ tool_name=self.tool_name,
82
+ banner=self.file_banner,
83
+ )
84
+
85
+ # -- output -------------------------------------------------------
86
+
87
+ def _formatter(self, override: Formatter | None) -> Formatter | None:
88
+ return override if override is not None else self.formatter
89
+
90
+ def render_hpp(self, *, formatter: Formatter | None = None) -> str:
91
+ """Render the header as text."""
92
+ text = render_hpp(self.build())
93
+ chosen = self._formatter(formatter)
94
+ return chosen(text, self.hpp_name) if chosen else text
95
+
96
+ def render_cpp(
97
+ self, cpp_file: str | None = None, *, formatter: Formatter | None = None
98
+ ) -> str:
99
+ """Render one source file as text. ``None`` selects the default one."""
100
+ text = render_cpp(self.build(), cpp_file)
101
+ chosen = self._formatter(formatter)
102
+ if not chosen:
103
+ return text
104
+ name = f"{cpp_file}.{self.cpp_extension}" if cpp_file else self.cpp_name
105
+ return chosen(text, name)
106
+
107
+ def files(
108
+ self,
109
+ cpp_files: Sequence[str] | None = None,
110
+ *,
111
+ formatter: Formatter | None = None,
112
+ ) -> dict[str, str]:
113
+ """Render every file this document owns, as a name-to-text mapping."""
114
+ return doc_files(self.build(), cpp_files, formatter=self._formatter(formatter))
115
+
116
+ def write(
117
+ self,
118
+ directory: str | Path = ".",
119
+ cpp_files: Sequence[str] | None = None,
120
+ *,
121
+ formatter: Formatter | None = None,
122
+ skip_unchanged: bool = True,
123
+ encoding: str = "utf-8",
124
+ ) -> WriteResult:
125
+ """Write every file this document owns into ``directory``.
126
+
127
+ Supplemental source files are discovered automatically.
128
+ """
129
+ return write_doc(
130
+ self.build(),
131
+ directory,
132
+ cpp_files,
133
+ formatter=self._formatter(formatter),
134
+ skip_unchanged=skip_unchanged,
135
+ encoding=encoding,
136
+ )
137
+
138
+ def __enter__(self) -> CppDocBuilder:
139
+ return self
140
+
141
+ def __exit__(self, *exc: object) -> None:
142
+ return None
143
+
144
+
145
+ def _default_guard(
146
+ file_base: str, namespaces: Sequence[str], hpp_extension: str
147
+ ) -> str:
148
+ """Derive an include-guard macro from the file base, namespaces and extension.
149
+
150
+ ``_default_guard("MyClass", ["Fw", "Cfg"], "hpp")`` gives ``"Fw_Cfg_MyClass_HPP"``.
151
+ Namespace arguments may themselves be qualified with ``::`` or ``.``.
152
+ """
153
+ parts = [part for ns in namespaces for part in re.split(r"::|\.", ns) if part]
154
+ ident = re.sub(r"[^A-Za-z0-9_]+", "_", "_".join([*parts, file_base])).strip("_")
155
+ return f"{ident}_{hpp_extension.upper()}"