fprime-cpp-codegen 0.1.0__py3-none-any.whl → 0.2.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.
@@ -10,6 +10,7 @@ The package is layered, and you can enter at whichever level suits the job:
10
10
  * :mod:`~fprime_cpp_codegen.comments` -- comment and banner formatting.
11
11
  * :mod:`~fprime_cpp_codegen.lines` -- the line model everything is built on.
12
12
  * :mod:`~fprime_cpp_codegen.output` -- rendering to text and to disk.
13
+ * :mod:`~fprime_cpp_codegen.validation` -- whole-document checks.
13
14
  * :mod:`~fprime_cpp_codegen.formatting` -- optional post-processing through
14
15
  ``clang-format``.
15
16
  * :mod:`~fprime_cpp_codegen.fprime` -- F Prime conventions. Import it explicitly;
@@ -52,6 +53,7 @@ from .doc import (
52
53
  VOID,
53
54
  Class,
54
55
  ClassMember,
56
+ Comment,
55
57
  Constructor,
56
58
  CppDoc,
57
59
  DefaultFileBanner,
@@ -83,6 +85,7 @@ from .lines import (
83
85
  wrap_in_scope,
84
86
  )
85
87
  from .output import WriteResult, collect_cpp_files, doc_files, write_doc
88
+ from .validation import check_document, orphaned_members, unfilled_definitions
86
89
  from .writer import (
87
90
  Context,
88
91
  CppWriter,
@@ -108,6 +111,7 @@ __all__ = [
108
111
  "VOID",
109
112
  "Class",
110
113
  "ClassMember",
114
+ "Comment",
111
115
  "Constructor",
112
116
  "CppDoc",
113
117
  "DefaultFileBanner",
@@ -155,6 +159,10 @@ __all__ = [
155
159
  "collect_cpp_files",
156
160
  "doc_files",
157
161
  "write_doc",
162
+ # Validation
163
+ "check_document",
164
+ "orphaned_members",
165
+ "unfilled_definitions",
158
166
  # Errors
159
167
  "CppCodegenError",
160
168
  "ScopeError",
@@ -34,6 +34,7 @@ from .comments import (
34
34
  write_comment_body,
35
35
  write_doxygen_comment,
36
36
  )
37
+ from .doc import Comment
37
38
  from .errors import ScopeError, ValidationError
38
39
  from .lines import Line, blank, indent_lines
39
40
  from .lines import line as _line
@@ -44,7 +45,9 @@ __all__ = ["Body", "Code", "Switch", "stmts"]
44
45
 
45
46
  #: Anything usable as a run of C++ statements. ``None`` contributes nothing, so
46
47
  #: ``b.add(frag if condition else None)`` needs no branch. A ``str`` is
47
- #: margin-stripped and taken verbatim, with no punctuation added.
48
+ #: margin-stripped and taken verbatim, with no punctuation added; pass a
49
+ #: :class:`~fprime_cpp_codegen.lines.Line` instead, or use :meth:`Body.line`, for text
50
+ #: that must survive a leading ``|``.
48
51
  Code: TypeAlias = "None | str | Line | Body | Sequence[Code]"
49
52
 
50
53
 
@@ -216,9 +219,14 @@ class Body:
216
219
  """Append one line verbatim."""
217
220
  return self._emit([_line(text)])
218
221
 
219
- def lines(self, text: str) -> Body:
220
- """Append a margin-stripped, possibly multi-line block of C++."""
221
- return self._emit(_lines(text))
222
+ def lines(self, text: str, *, margin: str | None = "|") -> Body:
223
+ """Append a margin-stripped, possibly multi-line block of C++.
224
+
225
+ ``margin=None`` turns the stripping off, for text taken from a generator's
226
+ input that may legitimately begin with the margin character. :meth:`line`
227
+ never strips.
228
+ """
229
+ return self._emit(_lines(text, margin=margin))
222
230
 
223
231
  def raw(self, ll: Iterable[Line]) -> Body:
224
232
  """Append already-rendered lines."""
@@ -241,19 +249,19 @@ class Body:
241
249
  # Comments
242
250
  # ------------------------------------------------------------------
243
251
 
244
- def comment(self, text: str) -> Body:
252
+ def comment(self, text: Comment) -> Body:
245
253
  """Append a ``//`` comment with no leading blank line."""
246
254
  return self._emit(write_comment_body(text))
247
255
 
248
- def spaced_comment(self, text: str) -> Body:
256
+ def spaced_comment(self, text: Comment) -> Body:
249
257
  """Append a ``//`` comment preceded by a blank line."""
250
258
  return self._emit(write_comment(text))
251
259
 
252
- def doc_comment(self, text: str) -> Body:
260
+ def doc_comment(self, text: Comment) -> Body:
253
261
  """Append a ``//!`` doxygen comment."""
254
262
  return self._emit(write_doxygen_comment(text))
255
263
 
256
- def banner(self, text: str) -> Body:
264
+ def banner(self, text: Comment) -> Body:
257
265
  """Append a ruled banner comment."""
258
266
  return self._emit(write_banner_comment(text))
259
267
 
@@ -95,3 +95,14 @@ def _extends(extends: str | Sequence[str] | None) -> str | None:
95
95
  return extends
96
96
  joined = ", ".join(extends)
97
97
  return joined or None
98
+
99
+
100
+ def _as_attributes(attributes: str | Sequence[str]) -> tuple[str, ...]:
101
+ """Normalise a declaration-attribute specification into a tuple.
102
+
103
+ A single string is one attribute, not a sequence of characters, since one is the
104
+ common case: ``attributes='__attribute__((visibility("default")))'``.
105
+ """
106
+ if isinstance(attributes, str):
107
+ return (attributes,)
108
+ return tuple(attributes)
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any
12
12
  from ..comments import write_access_tag, write_banner_comment
13
13
  from ..doc import (
14
14
  Class,
15
+ Comment,
15
16
  Constructor,
16
17
  Destructor,
17
18
  Function,
@@ -90,7 +91,7 @@ class _SectionBanner(_Builder[Lines]):
90
91
  definitions went.
91
92
  """
92
93
 
93
- def __init__(self, comment: str) -> None:
94
+ def __init__(self, comment: Comment) -> None:
94
95
  self.comment = comment
95
96
  self.members: list[object] = []
96
97
 
@@ -179,7 +180,7 @@ class AccessSection:
179
180
  its label and banner back out again.
180
181
  """
181
182
 
182
- def __init__(self, scope: ClassBuilder, tag: str, comment: str | None) -> None:
183
+ def __init__(self, scope: ClassBuilder, tag: str, comment: Comment | None) -> None:
183
184
  self._scope = scope
184
185
  self._count = 1
185
186
  scope._add(Lines(write_access_tag(tag), Output.HPP))
@@ -11,12 +11,22 @@ from ..comments import (
11
11
  write_doxygen_comment,
12
12
  write_doxygen_comment_opt,
13
13
  )
14
- from ..doc import Constructor, Destructor, Function, Lines, Output, Param, Type, as_type
14
+ from ..doc import (
15
+ Comment,
16
+ Constructor,
17
+ Destructor,
18
+ Function,
19
+ Lines,
20
+ Output,
21
+ Param,
22
+ Type,
23
+ as_type,
24
+ )
15
25
  from ..errors import ValidationError
16
26
  from ..lines import Line, wrap_in_scope
17
27
  from ..lines import line as _line
18
28
  from .base import _Builder
19
- from .coercion import _as_body_lines, _as_params, _sv_qualifier
29
+ from .coercion import _as_attributes, _as_body_lines, _as_params, _sv_qualifier
20
30
 
21
31
 
22
32
  class Radix(Enum):
@@ -53,7 +63,7 @@ class FunctionBuilder(_Builder[Function]):
53
63
  *,
54
64
  ret: Type | str = "void",
55
65
  params: Iterable[Param | Sequence[str]] = (),
56
- comment: str | None = None,
66
+ comment: Comment | None = None,
57
67
  body: Code = None,
58
68
  const: bool = False,
59
69
  static: bool = False,
@@ -66,6 +76,8 @@ class FunctionBuilder(_Builder[Function]):
66
76
  noexcept: bool = False,
67
77
  deleted: bool = False,
68
78
  defaulted: bool = False,
79
+ declaration_only: bool = False,
80
+ attributes: str | Sequence[str] = (),
69
81
  template: str | None = None,
70
82
  inline_body: bool = False,
71
83
  cpp_file: str | None = None,
@@ -81,6 +93,8 @@ class FunctionBuilder(_Builder[Function]):
81
93
  self.noexcept = noexcept
82
94
  self.deleted = deleted
83
95
  self.defaulted = defaulted
96
+ self.declaration_only = declaration_only
97
+ self.attributes = _as_attributes(attributes)
84
98
  self.template = template
85
99
  self.inline_body = inline_body
86
100
  self.cpp_file = cpp_file
@@ -99,7 +113,7 @@ class FunctionBuilder(_Builder[Function]):
99
113
  type_name: Type | str,
100
114
  name: str,
101
115
  *,
102
- comment: str | None = None,
116
+ comment: Comment | None = None,
103
117
  default: str | None = None,
104
118
  ) -> FunctionBuilder:
105
119
  """Append one formal parameter. Returns self, so calls can be chained."""
@@ -125,6 +139,8 @@ class FunctionBuilder(_Builder[Function]):
125
139
  noexcept=self.noexcept,
126
140
  deleted=self.deleted,
127
141
  defaulted=self.defaulted,
142
+ declaration_only=self.declaration_only,
143
+ attributes=self.attributes,
128
144
  template=self.template,
129
145
  inline_body=self.inline_body,
130
146
  cpp_file=self.cpp_file,
@@ -145,13 +161,14 @@ class ConstructorBuilder(_Builder[Constructor]):
145
161
  *,
146
162
  params: Iterable[Param | Sequence[str]] = (),
147
163
  initializers: Iterable[str] = (),
148
- comment: str | None = None,
164
+ comment: Comment | None = None,
149
165
  body: Code = None,
150
166
  explicit: bool = False,
151
167
  constexpr: bool = False,
152
168
  noexcept: bool = False,
153
169
  deleted: bool = False,
154
170
  defaulted: bool = False,
171
+ declaration_only: bool = False,
155
172
  template: str | None = None,
156
173
  inline_body: bool = False,
157
174
  cpp_file: str | None = None,
@@ -162,6 +179,7 @@ class ConstructorBuilder(_Builder[Constructor]):
162
179
  self.noexcept = noexcept
163
180
  self.deleted = deleted
164
181
  self.defaulted = defaulted
182
+ self.declaration_only = declaration_only
165
183
  self.template = template
166
184
  self.inline_body = inline_body
167
185
  self.cpp_file = cpp_file
@@ -174,7 +192,7 @@ class ConstructorBuilder(_Builder[Constructor]):
174
192
  type_name: Type | str,
175
193
  name: str,
176
194
  *,
177
- comment: str | None = None,
195
+ comment: Comment | None = None,
178
196
  default: str | None = None,
179
197
  ) -> ConstructorBuilder:
180
198
  """Append one formal parameter."""
@@ -202,6 +220,7 @@ class ConstructorBuilder(_Builder[Constructor]):
202
220
  noexcept=self.noexcept,
203
221
  deleted=self.deleted,
204
222
  defaulted=self.defaulted,
223
+ declaration_only=self.declaration_only,
205
224
  template=self.template,
206
225
  inline_body=self.inline_body,
207
226
  cpp_file=self.cpp_file,
@@ -220,13 +239,14 @@ class DestructorBuilder(_Builder[Destructor]):
220
239
  def __init__(
221
240
  self,
222
241
  *,
223
- comment: str | None = None,
242
+ comment: Comment | None = None,
224
243
  body: Code = None,
225
244
  virtual: bool = False,
226
245
  override: bool = False,
227
246
  noexcept: bool = False,
228
247
  deleted: bool = False,
229
248
  defaulted: bool = False,
249
+ declaration_only: bool = False,
230
250
  inline_body: bool = False,
231
251
  cpp_file: str | None = None,
232
252
  ) -> None:
@@ -236,6 +256,7 @@ class DestructorBuilder(_Builder[Destructor]):
236
256
  self.noexcept = noexcept
237
257
  self.deleted = deleted
238
258
  self.defaulted = defaulted
259
+ self.declaration_only = declaration_only
239
260
  self.inline_body = inline_body
240
261
  self.cpp_file = cpp_file
241
262
  self.body = Body(_as_body_lines(body))
@@ -249,6 +270,7 @@ class DestructorBuilder(_Builder[Destructor]):
249
270
  noexcept=self.noexcept,
250
271
  deleted=self.deleted,
251
272
  defaulted=self.defaulted,
273
+ declaration_only=self.declaration_only,
252
274
  inline_body=self.inline_body,
253
275
  cpp_file=self.cpp_file,
254
276
  )
@@ -269,7 +291,7 @@ class EnumBuilder(_Builder[Lines]):
269
291
  *,
270
292
  underlying: str | None = None,
271
293
  scoped: bool = False,
272
- comment: str | None = None,
294
+ comment: Comment | None = None,
273
295
  output: Output = Output.HPP,
274
296
  cpp_file: str | None = None,
275
297
  radix: Radix = Radix.DECIMAL,
@@ -323,7 +345,7 @@ class EnumBuilder(_Builder[Lines]):
323
345
  name: str,
324
346
  value: int | str | None = None,
325
347
  *,
326
- comment: str | None = None,
348
+ comment: Comment | None = None,
327
349
  radix: Radix | None = None,
328
350
  ) -> EnumBuilder:
329
351
  """Append one enumerator.
@@ -10,7 +10,8 @@ from ..doc import CppDoc, FileBanner, HppFile
10
10
  from ..errors import ValidationError
11
11
  from ..formatting import Formatter
12
12
  from ..output import WriteResult, doc_files, write_doc
13
- from ..writer import render_cpp, render_hpp
13
+ from ..validation import check_document
14
+ from ..writer import CppWriter, HppWriter, render_cpp, render_hpp
14
15
  from .base import _DocContext
15
16
  from .scopes import _MemberScope
16
17
 
@@ -28,8 +29,13 @@ class CppDocBuilder(_MemberScope[CppDoc]):
28
29
  tool_name: str | None = None,
29
30
  file_banner: FileBanner | None = None,
30
31
  formatter: Formatter | None = None,
32
+ hpp_writer: HppWriter | None = None,
33
+ cpp_writer: CppWriter | None = None,
31
34
  hpp_extension: str = "hpp",
32
35
  cpp_extension: str = "cpp",
36
+ emit_hpp: bool = True,
37
+ emit_cpp: bool = True,
38
+ strict: bool = False,
33
39
  ) -> None:
34
40
  """Start a document whose files are named after ``file_base``.
35
41
 
@@ -39,7 +45,20 @@ class CppDocBuilder(_MemberScope[CppDoc]):
39
45
 
40
46
  ``formatter`` post-processes every file this document renders; see
41
47
  :mod:`fprime_cpp_codegen.formatting`. Any render or write call can override
42
- it, but cannot switch it off.
48
+ it, but cannot switch it off. ``hpp_writer`` and ``cpp_writer`` work the same
49
+ way, substituting a :class:`~fprime_cpp_codegen.writer.DocWriter` subclass for
50
+ the default rendering.
51
+
52
+ ``emit_hpp=False`` or ``emit_cpp=False`` makes this a one-file document, which
53
+ a translation unit holding only a module-initialisation block wants. Members
54
+ that would then have nowhere to go raise :class:`ValidationError` when the
55
+ document is built, rather than disappearing.
56
+
57
+ ``strict=True`` additionally rejects any definition that needs a body and has
58
+ none, which would otherwise render as an empty out-of-line definition -- valid
59
+ C++, and so easy for a generator to emit by accident. Say
60
+ ``declaration_only=True`` to declare without defining, or ``body=""`` for a
61
+ definition that is deliberately empty.
43
62
  """
44
63
  super().__init__(_DocContext())
45
64
  if not file_base:
@@ -52,6 +71,23 @@ class CppDocBuilder(_MemberScope[CppDoc]):
52
71
  self.formatter = formatter
53
72
  """Applied to every file this document renders, unless a call overrides it."""
54
73
 
74
+ self.hpp_writer = hpp_writer
75
+ """Renders the header, unless a call overrides it. ``None`` uses
76
+ :class:`~fprime_cpp_codegen.writer.HppWriter`."""
77
+
78
+ self.cpp_writer = cpp_writer
79
+ """Renders the source files, unless a call overrides it. ``None`` uses
80
+ :class:`~fprime_cpp_codegen.writer.CppWriter`."""
81
+
82
+ self.emit_hpp = emit_hpp
83
+ """Whether this document produces a header at all."""
84
+
85
+ self.emit_cpp = emit_cpp
86
+ """Whether this document produces any source file at all."""
87
+
88
+ self.strict = strict
89
+ """Whether :meth:`build` rejects a definition that needs a body and has none."""
90
+
55
91
  self.file_banner = file_banner
56
92
  """Overrides the ``\\title``/``\\author``/``\\brief`` block atop each file.
57
93
  Distinct from :meth:`banner`, which emits a section comment."""
@@ -73,7 +109,8 @@ class CppDocBuilder(_MemberScope[CppDoc]):
73
109
  return f"{self.file_base}.{self.cpp_extension}"
74
110
 
75
111
  def build(self) -> CppDoc:
76
- return CppDoc(
112
+ """Produce the IR, checking it against ``emit_hpp``/``emit_cpp``/``strict``."""
113
+ doc = CppDoc(
77
114
  description=self.description,
78
115
  hpp_file=HppFile(self.hpp_name, self.include_guard),
79
116
  cpp_file_name=self.cpp_name,
@@ -81,23 +118,55 @@ class CppDocBuilder(_MemberScope[CppDoc]):
81
118
  tool_name=self.tool_name,
82
119
  banner=self.file_banner,
83
120
  )
121
+ check_document(
122
+ doc,
123
+ emit_hpp=self.emit_hpp,
124
+ emit_cpp=self.emit_cpp,
125
+ strict=self.strict,
126
+ )
127
+ return doc
84
128
 
85
129
  # -- output -------------------------------------------------------
86
130
 
87
131
  def _formatter(self, override: Formatter | None) -> Formatter | None:
88
132
  return override if override is not None else self.formatter
89
133
 
90
- def render_hpp(self, *, formatter: Formatter | None = None) -> str:
134
+ def _hpp_writer(self, override: HppWriter | None) -> HppWriter | None:
135
+ return override if override is not None else self.hpp_writer
136
+
137
+ def _cpp_writer(self, override: CppWriter | None) -> CppWriter | None:
138
+ return override if override is not None else self.cpp_writer
139
+
140
+ def _require_emitted(self, which: str) -> None:
141
+ """Reject rendering a file this document was told not to produce."""
142
+ if not (self.emit_hpp if which == "hpp" else self.emit_cpp):
143
+ raise ValidationError(
144
+ f"this document was built with emit_{which}=False, so it has no "
145
+ f"{which} file to render"
146
+ )
147
+
148
+ def render_hpp(
149
+ self,
150
+ *,
151
+ formatter: Formatter | None = None,
152
+ writer: HppWriter | None = None,
153
+ ) -> str:
91
154
  """Render the header as text."""
92
- text = render_hpp(self.build())
155
+ self._require_emitted("hpp")
156
+ text = render_hpp(self.build(), writer=self._hpp_writer(writer))
93
157
  chosen = self._formatter(formatter)
94
158
  return chosen(text, self.hpp_name) if chosen else text
95
159
 
96
160
  def render_cpp(
97
- self, cpp_file: str | None = None, *, formatter: Formatter | None = None
161
+ self,
162
+ cpp_file: str | None = None,
163
+ *,
164
+ formatter: Formatter | None = None,
165
+ writer: CppWriter | None = None,
98
166
  ) -> str:
99
167
  """Render one source file as text. ``None`` selects the default one."""
100
- text = render_cpp(self.build(), cpp_file)
168
+ self._require_emitted("cpp")
169
+ text = render_cpp(self.build(), cpp_file, writer=self._cpp_writer(writer))
101
170
  chosen = self._formatter(formatter)
102
171
  if not chosen:
103
172
  return text
@@ -109,9 +178,19 @@ class CppDocBuilder(_MemberScope[CppDoc]):
109
178
  cpp_files: Sequence[str] | None = None,
110
179
  *,
111
180
  formatter: Formatter | None = None,
181
+ hpp_writer: HppWriter | None = None,
182
+ cpp_writer: CppWriter | None = None,
112
183
  ) -> dict[str, str]:
113
184
  """Render every file this document owns, as a name-to-text mapping."""
114
- return doc_files(self.build(), cpp_files, formatter=self._formatter(formatter))
185
+ return doc_files(
186
+ self.build(),
187
+ cpp_files,
188
+ formatter=self._formatter(formatter),
189
+ hpp_writer=self._hpp_writer(hpp_writer),
190
+ cpp_writer=self._cpp_writer(cpp_writer),
191
+ emit_hpp=self.emit_hpp,
192
+ emit_cpp=self.emit_cpp,
193
+ )
115
194
 
116
195
  def write(
117
196
  self,
@@ -119,6 +198,8 @@ class CppDocBuilder(_MemberScope[CppDoc]):
119
198
  cpp_files: Sequence[str] | None = None,
120
199
  *,
121
200
  formatter: Formatter | None = None,
201
+ hpp_writer: HppWriter | None = None,
202
+ cpp_writer: CppWriter | None = None,
122
203
  skip_unchanged: bool = True,
123
204
  encoding: str = "utf-8",
124
205
  ) -> WriteResult:
@@ -131,6 +212,10 @@ class CppDocBuilder(_MemberScope[CppDoc]):
131
212
  directory,
132
213
  cpp_files,
133
214
  formatter=self._formatter(formatter),
215
+ hpp_writer=self._hpp_writer(hpp_writer),
216
+ cpp_writer=self._cpp_writer(cpp_writer),
217
+ emit_hpp=self.emit_hpp,
218
+ emit_cpp=self.emit_cpp,
134
219
  skip_unchanged=skip_unchanged,
135
220
  encoding=encoding,
136
221
  )