Skip to content

Commit ecc31fa

Browse files
Issue/351 (#381)
Generating _Static_assert unless c23 is specified.
1 parent b350209 commit ecc31fa

File tree

20 files changed

+922
-72
lines changed

20 files changed

+922
-72
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ jobs:
141141
matrix:
142142
architecture: [native32, native, arm-none-eabi]
143143
compiler: [gcc, clang]
144-
language: [c-11, c-11-arr-override, cpp-14, cpp-17, cpp-20]
144+
language: [c-11, c-11-arr-override, c-17, c-23, cpp-14, cpp-17, cpp-20]
145145
exclude:
146146
- architecture: native32
147147
compiler: clang

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Nunavut ships with built-in support for some programming languages,
3535
and it can be used to generate code for other languages if custom templates (and some glue logic) are provided.
3636
Currently, the following languages are supported out of the box:
3737

38-
- **C11** (generates header-only libraries)
38+
- **C11,c17,c23** (generates header-only libraries)
3939
- **C++** (generates header-only libraries; `work-in-progress <https://github.com/OpenCyphal/nunavut/issues/91>`_)
4040
- **Python** (generates Python packages)
4141
- **HTML** (generates documentation pages)

src/nunavut/_generators.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ def basic_language_context_builder_from_args(target_language: str, **kwargs: Any
250250
The following arguments are supported:
251251
252252
* **configuration**: A list of additional configuration files to load.
253+
* **option**: Key value arguments to override individual language option values.
253254
* **include_experimental_languages**: If true then experimental languages will also be available.
254255
* **language_options**: Opaque arguments passed through to the language objects. The supported arguments and
255256
valid values are different depending on the language specified by the :code:`language_key` parameter.
@@ -260,7 +261,7 @@ def basic_language_context_builder_from_args(target_language: str, **kwargs: Any
260261
:return: A new :class:`LanguageContextBuilder` object based on the command line arguments.
261262
"""
262263

263-
additional_config_files = kwargs.get("configuration", None)
264+
additional_config_files = kwargs.get("configuration")
264265
if additional_config_files is None:
265266
additional_config_files = []
266267
if isinstance(additional_config_files, Path):
@@ -279,6 +280,11 @@ def basic_language_context_builder_from_args(target_language: str, **kwargs: Any
279280
.add_config_files(*additional_config_files)
280281
)
281282

283+
options = kwargs.get("option")
284+
if options is not None:
285+
for option_key, option_value in options:
286+
builder.add_target_language_option_override(option_key, option_value)
287+
282288
return builder
283289

284290

src/nunavut/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"""
1010
import sys
1111

12-
__version__ = "3.0.0.dev1" # please update NunavutConfigVersion.cmake if changing the major or minor version.
12+
__version__ = "3.0.0.dev2" # please update NunavutConfigVersion.cmake if changing the major or minor version.
1313
__license__ = "MIT"
1414
__author__ = "OpenCyphal"
1515
__copyright__ = (

src/nunavut/cli/__init__.py

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import sys
1313
import textwrap
1414
from pathlib import Path
15-
from typing import Any, Optional, Type, TypeVar, cast
15+
from typing import Any, Optional, Tuple, Type, TypeVar, cast
1616

1717

1818
class _LazyVersionAction(argparse._VersionAction):
@@ -37,6 +37,26 @@ def __call__(
3737
parser.exit()
3838

3939

40+
def keyvalue(arg: str) -> Tuple[str, Any]:
41+
"""
42+
Parse a key=value string into a tuple.
43+
44+
:param arg: String in the format "key=value"
45+
:return: Tuple of (key, value)
46+
:raises ValueError: If the string is not in key=value format
47+
"""
48+
try:
49+
key, value = arg.split("=", 1)
50+
value_stripped: Any = value.strip()
51+
if value_stripped == "false":
52+
value_stripped = False
53+
elif value_stripped == "true":
54+
value_stripped = True
55+
return (key.strip(), value_stripped)
56+
except ValueError:
57+
raise ValueError(f'"{arg}" is not in key=value format') from None
58+
59+
4060
ParserT = TypeVar("ParserT")
4161
"""
4262
Type variable for the concrete parser to create.
@@ -310,14 +330,14 @@ def _make_parser(parser_type: Type[ParserT]) -> ParserT:
310330
311331
# looks for all_types.j2 in the c_jinja template directory and generates
312332
# generated/include/all_types.h from all types in all DSDL namespaces.
313-
nnvg --index-file all_types.h --outdir generated/include --templates c_jinja \
314-
path/to/types/animal:cat.1.0.dsdl \
333+
nnvg --index-file all_types.h --outdir generated/include --templates c_jinja \\
334+
path/to/types/animal:cat.1.0.dsdl \\
315335
path/to/types/animal:dog.1.0.dsdl
316336
317337
# looks for manifest.j2 in the json_jinja template directory and generates
318338
# generated/include/manifest.json from all types in all DSDL namespaces.
319-
nnvg --index-file include/manifest.json --outdir generated --templates json_jinja \
320-
path/to/types/animal:cat.1.0.dsdl \
339+
nnvg --index-file include/manifest.json --outdir generated --templates json_jinja \\
340+
path/to/types/animal:cat.1.0.dsdl \\
321341
path/to/types/animal:dog.1.0.dsdl
322342
323343
"""
@@ -344,7 +364,7 @@ def _make_parser(parser_type: Type[ParserT]) -> ParserT:
344364
)
345365

346366
def extension_type(raw_arg: str) -> str:
347-
if len(raw_arg) > 0 and not raw_arg.startswith("."):
367+
if raw_arg and not raw_arg.startswith("."):
348368
return "." + raw_arg
349369
else:
350370
return raw_arg
@@ -823,7 +843,7 @@ def extension_type(raw_arg: str) -> str:
823843
ln_opt_group.add_argument(
824844
"--language-standard",
825845
"-std",
826-
choices=["c11", "c++14", "cetl++14-17", "c++17", "c++17-pmr", "c++20", "c++20-pmr"],
846+
choices=["c11", "c17", "c23", "c++14", "cetl++14-17", "c++17", "c++17-pmr", "c++20", "c++20-pmr"],
827847
help=textwrap.dedent(
828848
"""
829849
@@ -870,6 +890,29 @@ def extension_type(raw_arg: str) -> str:
870890
).lstrip(),
871891
)
872892

893+
ln_opt_group.add_argument(
894+
"--option",
895+
"-o",
896+
nargs="*",
897+
type=keyvalue,
898+
help=textwrap.dedent(
899+
"""
900+
Passes a key=value pair to the template as a language option overriding default options where they are
901+
specified. This is useful for passing options to the template that are not available as command-line
902+
arguments. For example, if you have a template that uses the
903+
"foo" variable you can pass a value to it using this argument::
904+
905+
nnvg -o foo=value ...
906+
907+
where "foo" is then available in the template as `{{ options.foo }}`.
908+
909+
This option is similar to the `--configuration` option but only applies to the options section of the target
910+
language and doesn't require a file to be created.
911+
912+
"""
913+
).lstrip(),
914+
)
915+
873916
return cast(ParserT, parser)
874917

875918

src/nunavut/cli/parsers.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class NunavutArgumentParser(argparse.ArgumentParser):
100100
"""
101101

102102
DSDL_FILE_SUFFIXES = (".uavcan", ".dsdl")
103+
MAX_LOG_ENTRY_LENGTH = 1000
103104

104105
# --[ OVERRIDE ]--------------------------------------------------------------------------------------------------
105106
def parse_known_args(self, args=None, namespace=None): # type: ignore
@@ -108,13 +109,79 @@ def parse_known_args(self, args=None, namespace=None): # type: ignore
108109
return (parsed_args, argv)
109110

110111
# --[ PRIVATE ]---------------------------------------------------------------------------------------------------
112+
113+
@staticmethod
114+
def _sanitize_log_message(message: str) -> str:
115+
"""
116+
Sanitize a message before logging by:
117+
1. Removing control characters
118+
2. Replacing newlines and tabs with spaces
119+
3. Limiting length
120+
121+
:param: Raw message to sanitize
122+
123+
:return: Sanitized message safe for logging
124+
125+
.. invisible-code-block: python
126+
127+
from nunavut.cli.parsers import NunavutArgumentParser
128+
129+
# Control characters
130+
message = "This is a test message with control characters \\x00\\x01."
131+
sanitized_message = "This is a test message with control characters ."
132+
assert sanitized_message == NunavutArgumentParser._sanitize_log_message(message)
133+
134+
# Newlines and tabs
135+
message = "This is a\\r\\ntest\\tmessage with newlines and tabs.\\n\\t"
136+
sanitized_message = "This is a test message with newlines and tabs."
137+
assert sanitized_message == NunavutArgumentParser._sanitize_log_message(message)
138+
139+
# Multiple spaces
140+
message = "This is a test message with multiple spaces."
141+
sanitized_message = "This is a test message with multiple spaces."
142+
assert sanitized_message == NunavutArgumentParser._sanitize_log_message(message)
143+
144+
# Stripping whitespace
145+
message = " This is a test message with leading and trailing whitespace. "
146+
sanitized_message = "This is a test message with leading and trailing whitespace."
147+
assert sanitized_message == NunavutArgumentParser._sanitize_log_message(message)
148+
149+
# Length limit
150+
message_preamble = "This is a test message"
151+
message = message_preamble + "a" * NunavutArgumentParser.MAX_LOG_ENTRY_LENGTH
152+
sanitized_message = (
153+
message_preamble +
154+
"a" * (NunavutArgumentParser.MAX_LOG_ENTRY_LENGTH - len(message_preamble)) + "... (truncated)"
155+
)
156+
assert sanitized_message == NunavutArgumentParser._sanitize_log_message(message)
157+
158+
"""
159+
# Remove control characters except whitespace
160+
message = "".join(char for char in message if char.isprintable() or char.isspace())
161+
162+
# Replace newlines and tabs with spaces
163+
message = re.sub(r"[\n\r\t]+", " ", message)
164+
165+
# Collapse multiple spaces
166+
message = re.sub(r"\s+", " ", message)
167+
168+
# Trim whitespace
169+
message = message.strip()
170+
171+
# Limit length to prevent log flooding
172+
if len(message) > NunavutArgumentParser.MAX_LOG_ENTRY_LENGTH:
173+
message = message[: NunavutArgumentParser.MAX_LOG_ENTRY_LENGTH] + "... (truncated)"
174+
175+
return message
176+
111177
def _post_process_log(self, args: argparse.Namespace, message: str) -> None:
112178
"""
113179
Print a message to the log.
114180
"""
115181
if args.verbose <= 0:
116182
return
117-
self._print_message(message, sys.stdout)
183+
sanitary_message = self._sanitize_log_message(message)
184+
self._print_message(sanitary_message, sys.stdout)
118185

119186
def _post_process_args(self, args: argparse.Namespace) -> None:
120187
"""

src/nunavut/lang/__init__.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class LanguageContextBuilder:
6666

6767
def __init__(self, include_experimental_languages: bool = False):
6868
self._target_language_name: typing.Optional[str] = None
69-
self._target_language_config: typing.Dict[str, str] = {}
69+
self._target_language_config: typing.Dict[str, typing.Any] = {}
7070
self._ln_loader = LanguageClassLoader()
7171
self._include_experimental_languages = include_experimental_languages
7272

@@ -163,6 +163,82 @@ def set_target_language_configuration_override(self, key: str, value: typing.Any
163163
self._target_language_config[key] = value
164164
return self
165165

166+
def add_target_language_option_override(self, key: str, value: typing.Any) -> "LanguageContextBuilder":
167+
"""
168+
Adds a key and value to override in the language options section of the configuration for a language target
169+
when a LanguageContext is crated. These overrides are always set under the language section of the target
170+
language.
171+
172+
.. invisible-code-block: python
173+
174+
from nunavut.lang import LanguageContextBuilder, Language, LanguageClassLoader
175+
176+
.. code-block:: python
177+
178+
builder = LanguageContextBuilder().set_target_language("c")
179+
c_section_name = LanguageClassLoader.to_language_module_name("c")
180+
181+
default_c_language_options = builder.config.get_config_value_as_dict(
182+
c_section_name,
183+
Language.WKCV_LANGUAGE_OPTIONS)
184+
185+
assert default_c_language_options["std"] == "c11"
186+
187+
We can now try to override the default standard for a future "C" target language object:
188+
189+
.. code-block:: python
190+
191+
builder.add_target_language_option_override("std", "c17")
192+
193+
...but that value will not be overridden until you create the target language:
194+
195+
.. code-block:: python
196+
197+
198+
default_c_language_options = builder.config.get_config_value_as_dict(
199+
c_section_name,
200+
Language.WKCV_LANGUAGE_OPTIONS)
201+
202+
assert default_c_language_options["std"] == "c11"
203+
204+
_ = builder.create()
205+
206+
overridden_c_language_options = builder.config.get_config_value_as_dict(
207+
c_section_name,
208+
Language.WKCV_LANGUAGE_OPTIONS)
209+
210+
assert default_c_language_options["std"] == "c17"
211+
212+
Note that the config is scoped by the builder but is then inherited by the language objects created by the
213+
builder in the same way as the configuration overrides.
214+
215+
.. invisible-code-block: python
216+
217+
from pytest import raises
218+
219+
builder = LanguageContextBuilder().set_target_language("c")
220+
c_section_name = LanguageClassLoader.to_language_module_name("c")
221+
builder.set_target_language_configuration_override(
222+
Language.WKCV_LANGUAGE_OPTIONS,
223+
"wrong type"
224+
)
225+
with raises(ValueError):
226+
builder.add_target_language_option_override("std", "c17")
227+
# This will raise a ValueError because the WKCV_LANGUAGE_OPTIONS is not a dict.
228+
229+
"""
230+
if value is not None:
231+
options = self._target_language_config.get(Language.WKCV_LANGUAGE_OPTIONS)
232+
if options is None:
233+
options = {}
234+
self._target_language_config[Language.WKCV_LANGUAGE_OPTIONS] = options
235+
if not isinstance(options, dict):
236+
raise ValueError(
237+
f"Cannot set target language option override for {key} because the value is not a dict."
238+
)
239+
options[key] = value
240+
return self
241+
166242
def set_target_language_extension(
167243
self, target_language_extension: typing.Optional[str]
168244
) -> "LanguageContextBuilder":

0 commit comments

Comments
 (0)