Skip to content

Commit a1506ce

Browse files
Add support for strings and byte arrays (#97)
Implement string and byte array support as described in OpenCyphal/specification#51. The grammar has been modified as follows: - Add `utf8` primitive. - Add `byte` primitive. - Remove the unnecessary form `saturated bool`, which was shorthand for `bool`, for regularity. From now on, only unqualified `bool` is allowed. The expected impact is zero since the long form was never seen in the wild. Note that `truncated bool` was never legal. At the moment, the following constraints are enforced; they may be lifted in the future: - `utf8` can only be used as an element type of a variable-length array: ``` utf8[<=10] name # valid utf8[10] name # invalid utf8 name # invalid ``` - `byte` can only be used as an element type of an array: ``` byte[<=10] data # valid byte[10] data # valid byte data # invalid ``` Both `utf8` and `byte` are concrete instances of `UnsignedIntegerType`. By virtue of this, existing users of PyDSDL (like Nunavut) will interpret both `utf8` and `byte` as `truncated uint8`, until explicitly updated to take advantage of the new feature. That is: ``` utf8[<=10] name # seen by Nunavut as: truncated uint8[<=10] utf8[10] name # frontend error utf8 name # frontend error byte[<=10] data # seen by Nunavut as: truncated uint8[<=10] byte[10] data # seen by Nunavut as: truncated uint8[10] byte data # frontend error ``` Property `pydsdl.ArrayType.string_like` is now deprecated and should be replaced with an explicit `isinstance(array.element_type, pydsdl.UTF8Type)`. Some internal refactoring has been done to unify the deprecation consistency checking with the aggregation constraints listed above (e.g., `utf8[<=10]` is valid but `utf8[10]` is not). Closes #96 Relates to OpenCyphal/yakut#65
1 parent 00445d4 commit a1506ce

21 files changed

+344
-101
lines changed

.github/workflows/test-and-release.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
name: 'Test and Release PyDSDL'
22
on: [ push, pull_request ]
33

4-
# Ensures that only one workflow is running at a time
5-
concurrency:
6-
group: ${{ github.workflow_sha }}
7-
cancel-in-progress: true
8-
94
jobs:
105
pydsdl-test:
116
name: Test PyDSDL

noxfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def lint(session):
9898
)
9999
if is_latest_python(session):
100100
# we run black only on the newest Python version to ensure that the code is formatted with the latest version
101-
session.install("black ~= 24.4")
101+
session.install("black ~= 25.1")
102102
session.run("black", "--check", ".")
103103

104104

pydsdl/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
import sys as _sys
88
from pathlib import Path as _Path
99

10-
__version__ = "1.22.2"
10+
__version__ = "1.23.0"
1111
__version_info__ = tuple(map(int, __version__.split(".")[:3]))
1212
__license__ = "MIT"
1313
__author__ = "OpenCyphal"
14-
__copyright__ = "Copyright (c) 2018 OpenCyphal"
14+
__copyright__ = "Copyright (c) OpenCyphal development team"
1515
__email__ = "[email protected]"
1616

1717
# Our unorthodox approach to dependency management requires us to apply certain workarounds.
@@ -42,6 +42,8 @@
4242
from ._serializable import IntegerType as IntegerType
4343
from ._serializable import SignedIntegerType as SignedIntegerType
4444
from ._serializable import UnsignedIntegerType as UnsignedIntegerType
45+
from ._serializable import ByteType as ByteType
46+
from ._serializable import UTF8Type as UTF8Type
4547
from ._serializable import FloatType as FloatType
4648
from ._serializable import VoidType as VoidType
4749
from ._serializable import ArrayType as ArrayType

pydsdl/_bit_length_set/_bit_length_set.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ def elementwise_sum_k_multicombinations(self, k: int) -> "BitLengthSet": # prag
347347

348348
@staticmethod
349349
def elementwise_sum_cartesian_product(
350-
sets: typing.Iterable[typing.Union[typing.Iterable[int], int]]
350+
sets: typing.Iterable[typing.Union[typing.Iterable[int], int]],
351351
) -> "BitLengthSet": # pragma: no cover
352352
"""
353353
:meta private:

pydsdl/_dsdl.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ def read(
119119
definition_visitors: Iterable["DefinitionVisitor"],
120120
print_output_handler: Callable[[int, str], None],
121121
allow_unregulated_fixed_port_id: bool,
122+
*,
123+
strict: bool = False,
122124
) -> CompositeType:
123125
"""
124126
Reads the data type definition and returns its high-level data type representation.
@@ -133,6 +135,7 @@ def read(
133135
:param definition_visitors: Visitors to notify about discovered dependencies.
134136
:param print_output_handler: Used for @print and for diagnostics: (line_number, text) -> None.
135137
:param allow_unregulated_fixed_port_id: Do not complain about fixed unregulated port IDs.
138+
:param strict: Reject features that are not part of the Specification.
136139
:return: The data type representation.
137140
"""
138141
raise NotImplementedError()

pydsdl/_dsdl_definition.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ def read(
235235
definition_visitors: Iterable[DefinitionVisitor],
236236
print_output_handler: Callable[[int, str], None],
237237
allow_unregulated_fixed_port_id: bool,
238+
*,
239+
strict: bool = False,
238240
) -> CompositeType:
239241
log_prefix = "%s.%d.%d" % (self.full_name, self.version.major, self.version.minor)
240242
if self._cached_type is not None:
@@ -262,7 +264,7 @@ def read(
262264
allow_unregulated_fixed_port_id=allow_unregulated_fixed_port_id,
263265
)
264266

265-
_parser.parse(self.text, builder)
267+
_parser.parse(self.text, builder, strict=strict)
266268

267269
self._cached_type = builder.finalize()
268270
_logger.info(
@@ -536,5 +538,5 @@ def _unittest_type_from_path_inference_edge_case(temp_dsdl_factory) -> None: #
536538

537539
def _unittest_from_first_in(temp_dsdl_factory) -> None: # type: ignore
538540
dsdl_file = temp_dsdl_factory.new_file(Path("repo/uavcan/foo/bar/435.baz.1.0.dsdl"), "@sealed")
539-
dsdl_def = DSDLDefinition.from_first_in(dsdl_file.resolve(), [(dsdl_file.parent.parent / "..")])
541+
dsdl_def = DSDLDefinition.from_first_in(dsdl_file.resolve(), [dsdl_file.parent.parent / ".."])
540542
assert dsdl_def.full_name == "uavcan.foo.bar.baz"

pydsdl/_expression/_container.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class Set(Container):
3232
class _Decorator:
3333
@staticmethod
3434
def homotypic_binary_operator(
35-
inferior: typing.Callable[["Set", "Set"], _O]
35+
inferior: typing.Callable[["Set", "Set"], _O],
3636
) -> typing.Callable[["Set", "Set"], _O]:
3737
def wrapper(self: "Set", other: "Set") -> _O:
3838
assert isinstance(self, Set) and isinstance(other, Set)

pydsdl/_namespace.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ def read_namespace(
7474
print_output_handler: PrintOutputHandler | None = None,
7575
allow_unregulated_fixed_port_id: bool = False,
7676
allow_root_namespace_name_collision: bool = True,
77+
*,
78+
strict: bool = False,
7779
) -> list[_serializable.CompositeType]:
7880
"""
7981
This function is a main entry point for the library.
@@ -103,6 +105,8 @@ def read_namespace(
103105
the same root namespace name multiple times in the lookup dirs. This will enable defining a namespace
104106
partially and let other entities define new messages or new sub-namespaces in the same root namespace.
105107
108+
:param strict: Reject features that are not [yet] part of the Cyphal Specification.
109+
106110
:return: A list of :class:`pydsdl.CompositeType` found under the `root_namespace_directory` and sorted
107111
lexicographically by full data type name, then by major version (newest version first), then by minor
108112
version (newest version first). The ordering guarantee allows the caller to always find the newest version
@@ -131,7 +135,11 @@ def read_namespace(
131135
_logger.debug(_LOG_LIST_ITEM_PREFIX + str(x))
132136

133137
return _complete_read_function(
134-
target_dsdl_definitions, lookup_directories_path_list, print_output_handler, allow_unregulated_fixed_port_id
138+
target_dsdl_definitions,
139+
lookup_directories_path_list,
140+
print_output_handler,
141+
allow_unregulated_fixed_port_id=allow_unregulated_fixed_port_id,
142+
strict=strict,
135143
).direct
136144

137145

@@ -142,6 +150,8 @@ def read_files(
142150
lookup_directories: None | Path | str | Iterable[Path | str] = None,
143151
print_output_handler: PrintOutputHandler | None = None,
144152
allow_unregulated_fixed_port_id: bool = False,
153+
*,
154+
strict: bool = False,
145155
) -> tuple[list[_serializable.CompositeType], list[_serializable.CompositeType]]:
146156
"""
147157
This function is a main entry point for the library.
@@ -232,6 +242,8 @@ def read_files(
232242
This is a dangerous feature that must not be used unless you understand the risks.
233243
Please read https://opencyphal.org/guide.
234244
245+
:param strict: Reject features that are not [yet] part of the Cyphal Specification.
246+
235247
:return: A Tuple of lists of :class:`pydsdl.CompositeType`. The first index in the Tuple are the types parsed from
236248
the ``dsdl_files`` argument. The second index are types that the target ``dsdl_files`` utilizes.
237249
A note for using these values to describe build dependencies: each :class:`pydsdl.CompositeType` has two
@@ -266,9 +278,12 @@ def read_files(
266278
)
267279

268280
definitions = _complete_read_function(
269-
target_dsdl_definitions, lookup_directories_path_list, print_output_handler, allow_unregulated_fixed_port_id
281+
target_dsdl_definitions,
282+
lookup_directories_path_list,
283+
print_output_handler,
284+
allow_unregulated_fixed_port_id=allow_unregulated_fixed_port_id,
285+
strict=strict,
270286
)
271-
272287
return (definitions.direct, definitions.transitive)
273288

274289

@@ -286,7 +301,9 @@ def _complete_read_function(
286301
target_dsdl_definitions: SortedFileList[ReadableDSDLFile],
287302
lookup_directories_path_list: list[Path],
288303
print_output_handler: PrintOutputHandler | None,
304+
*,
289305
allow_unregulated_fixed_port_id: bool,
306+
strict: bool,
290307
) -> DSDLDefinitions:
291308

292309
lookup_dsdl_definitions = _construct_dsdl_definitions_from_namespaces(lookup_directories_path_list)
@@ -307,7 +324,11 @@ def _complete_read_function(
307324
# This is the biggie. All the rest of the wrangling is just to get to this point. This will take the
308325
# most time and memory.
309326
definitions = read_definitions(
310-
target_dsdl_definitions, lookup_dsdl_definitions, print_output_handler, allow_unregulated_fixed_port_id
327+
target_dsdl_definitions,
328+
lookup_dsdl_definitions,
329+
print_output_handler,
330+
allow_unregulated_fixed_port_id=allow_unregulated_fixed_port_id,
331+
strict=strict,
311332
)
312333

313334
# Note that we check for collisions in the read namespace only.

pydsdl/_namespace_reader.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
def _read_definitions(
2020
target_definitions: SortedFileList[ReadableDSDLFile],
2121
lookup_definitions: SortedFileList[ReadableDSDLFile],
22+
*,
2223
print_output_handler: PrintOutputHandler | None,
2324
allow_unregulated_fixed_port_id: bool,
25+
strict: bool,
2426
direct: set[CompositeType],
2527
transitive: set[CompositeType],
2628
file_pool: dict[Path, ReadableDSDLFile],
27-
level: int,
29+
level: int = 0,
2830
) -> None:
2931
"""
3032
Don't look at me! I'm hideous!
@@ -87,12 +89,13 @@ def print_handler(file: Path, line: int, message: str) -> None:
8789
_read_definitions(
8890
dsdl_file_sort(_pending_definitions),
8991
lookup_definitions,
90-
print_output_handler,
91-
allow_unregulated_fixed_port_id,
92-
direct,
93-
transitive,
94-
file_pool,
95-
level + 1,
92+
print_output_handler=print_output_handler,
93+
allow_unregulated_fixed_port_id=allow_unregulated_fixed_port_id,
94+
strict=strict,
95+
direct=direct,
96+
transitive=transitive,
97+
file_pool=file_pool,
98+
level=level + 1,
9699
)
97100
_pending_definitions.clear()
98101

@@ -116,6 +119,8 @@ def read_definitions(
116119
lookup_definitions: SortedFileList[ReadableDSDLFile],
117120
print_output_handler: PrintOutputHandler | None,
118121
allow_unregulated_fixed_port_id: bool,
122+
*,
123+
strict: bool = False,
119124
) -> DSDLDefinitions:
120125
"""
121126
Given a set of DSDL files, this method reads the text and invokes the parser for each and for any files found in the
@@ -125,6 +130,7 @@ def read_definitions(
125130
:param lookup_definitions: List of definitions available for referring to.
126131
:param print_output_handler: Used for @print and for diagnostics: (line_number, text) -> None.
127132
:param allow_unregulated_fixed_port_id: Do not complain about fixed unregulated port IDs.
133+
:param strict: Reject features that are not part of the Specification.
128134
:return: The data type representation.
129135
:raises InvalidDefinitionError: If a dependency is missing.
130136
:raises InternalError: If an unexpected error occurs.
@@ -135,12 +141,12 @@ def read_definitions(
135141
_read_definitions(
136142
target_definitions,
137143
lookup_definitions,
138-
print_output_handler,
139-
allow_unregulated_fixed_port_id,
140-
_direct,
141-
_transitive,
142-
_file_pool,
143-
0,
144+
print_output_handler=print_output_handler,
145+
allow_unregulated_fixed_port_id=allow_unregulated_fixed_port_id,
146+
strict=strict,
147+
direct=_direct,
148+
transitive=_transitive,
149+
file_pool=_file_pool,
144150
)
145151
return DSDLDefinitions(
146152
dsdl_file_sort(_direct),
@@ -186,6 +192,7 @@ def _unittest_namespace_reader_read_definitions_multiple(temp_dsdl_factory) -> N
186192
assert len(definitions.transitive) == 2
187193

188194

195+
# noinspection PyProtectedMember
189196
def _unittest_namespace_reader_read_definitions_multiple_no_load(temp_dsdl_factory) -> None: # type: ignore
190197
"""
191198
Ensure that the loader does not load files that are not in the transitive closure of the target files.

pydsdl/_parser.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ class DSDLSyntaxError(_error.InvalidDefinitionError):
2121
pass
2222

2323

24-
def parse(text: str, statement_stream_processor: "StatementStreamProcessor") -> None:
24+
def parse(text: str, statement_stream_processor: "StatementStreamProcessor", *, strict: bool) -> None:
2525
"""
2626
The entry point of the parser. As the text is being parsed, the parser invokes appropriate
2727
methods in the statement stream processor.
2828
"""
29-
pr = _ParseTreeProcessor(statement_stream_processor)
29+
pr = _ParseTreeProcessor(statement_stream_processor, strict=strict)
3030
try:
3131
pr.visit(_get_grammar().parse(text)) # type: ignore
3232
except _error.FrontendError as ex:
@@ -134,12 +134,13 @@ class _ParseTreeProcessor(parsimonious.NodeVisitor):
134134
# Beware that those might be propagated from recursive parser instances!
135135
unwrapped_exceptions = (_error.FrontendError, SystemError, MemoryError, SystemExit) # type: ignore
136136

137-
def __init__(self, statement_stream_processor: StatementStreamProcessor):
137+
def __init__(self, statement_stream_processor: StatementStreamProcessor, *, strict: bool):
138138
assert isinstance(statement_stream_processor, StatementStreamProcessor)
139139
self._statement_stream_processor = statement_stream_processor # type: StatementStreamProcessor
140140
self._current_line_number = 1 # Lines are numbered from one
141141
self._comment = ""
142142
self._comment_is_header = True
143+
self._strict = bool(strict)
143144
super().__init__()
144145

145146
@property
@@ -265,6 +266,15 @@ def visit_type_version_specifier(self, _n: _Node, children: _Children) -> _seria
265266
assert isinstance(major, _expression.Rational) and isinstance(minor, _expression.Rational)
266267
return _serializable.Version(major=major.as_native_integer(), minor=minor.as_native_integer())
267268

269+
def visit_type_primitive_boolean(self, _n: _Node, _c: _Children) -> _serializable.PrimitiveType:
270+
return _serializable.BooleanType()
271+
272+
def visit_type_primitive_byte(self, _n: _Node, _c: _Children) -> _serializable.PrimitiveType:
273+
return _serializable.ByteType()
274+
275+
def visit_type_primitive_utf8(self, _n: _Node, _c: _Children) -> _serializable.PrimitiveType:
276+
return _serializable.UTF8Type()
277+
268278
def visit_type_primitive_truncated(self, _n: _Node, children: _Children) -> _serializable.PrimitiveType:
269279
_kw, _sp, cons = cast(Tuple[_Node, _Node, _PrimitiveTypeConstructor], children)
270280
return cons(_serializable.PrimitiveType.CastMode.TRUNCATED)
@@ -273,9 +283,6 @@ def visit_type_primitive_saturated(self, _n: _Node, children: _Children) -> _ser
273283
_, cons = cast(Tuple[_Node, _PrimitiveTypeConstructor], children)
274284
return cons(_serializable.PrimitiveType.CastMode.SATURATED)
275285

276-
def visit_type_primitive_name_boolean(self, _n: _Node, _c: _Children) -> _PrimitiveTypeConstructor:
277-
return typing.cast(_PrimitiveTypeConstructor, _serializable.BooleanType)
278-
279286
def visit_type_primitive_name_unsigned_integer(self, _n: _Node, children: _Children) -> _PrimitiveTypeConstructor:
280287
return lambda cm: _serializable.UnsignedIntegerType(children[-1], cm)
281288

0 commit comments

Comments
 (0)