Skip to content

Commit 946ca9f

Browse files
Add a strict mode option. In the future we should refactor all options into a single Options dataclass.
1 parent 3ef1ecf commit 946ca9f

File tree

9 files changed

+85
-27
lines changed

9 files changed

+85
-27
lines changed

pydsdl/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
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.

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: 3 additions & 1 deletion
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(

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: 8 additions & 3 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
@@ -269,9 +270,13 @@ def visit_type_primitive_boolean(self, _n: _Node, _c: _Children) -> _serializabl
269270
return _serializable.BooleanType()
270271

271272
def visit_type_primitive_byte(self, _n: _Node, _c: _Children) -> _serializable.PrimitiveType:
273+
if self._strict:
274+
raise _error.InvalidDefinitionError("byte is a non-standard extension unavailable in strict mode")
272275
return _serializable.ByteType()
273276

274277
def visit_type_primitive_utf8(self, _n: _Node, _c: _Children) -> _serializable.PrimitiveType:
278+
if self._strict:
279+
raise _error.InvalidDefinitionError("utf8 is a non-standard extension unavailable in strict mode")
275280
return _serializable.UTF8Type()
276281

277282
def visit_type_primitive_truncated(self, _n: _Node, children: _Children) -> _serializable.PrimitiveType:

pydsdl/_serializable/_composite.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from ._attribute import Attribute, Field, PaddingField, Constant
1515
from ._name import check_name, InvalidNameError
1616
from ._primitive import PrimitiveType, UnsignedIntegerType
17-
from ._void import VoidType
1817

1918
Version = typing.NamedTuple("Version", [("major", int), ("minor", int)])
2019

pydsdl/_test.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77

88
from __future__ import annotations
99
import tempfile
10-
from typing import Sequence, Type, Iterable
10+
from typing import Sequence, Type, Iterable, Any
1111
from pathlib import Path
1212
from textwrap import dedent
1313
import pytest # This is only safe to import in test files!
14-
from . import _data_type_builder
14+
from . import _data_type_builder, InvalidDefinitionError
1515
from . import _expression
1616
from . import _error
1717
from . import _parser
@@ -67,13 +67,16 @@ def is_fs_case_sensitive(self) -> bool:
6767

6868

6969
def parse_definition(
70-
definition: _dsdl_definition.DSDLDefinition, lookup_definitions: Sequence[_dsdl_definition.DSDLDefinition]
70+
definition: _dsdl_definition.DSDLDefinition,
71+
lookup_definitions: Sequence[_dsdl_definition.DSDLDefinition],
72+
**kwargs: Any,
7173
) -> _serializable.CompositeType:
7274
return definition.read(
7375
lookup_definitions,
7476
[],
7577
print_output_handler=lambda line, text: print("Output from line %d:" % line, text),
7678
allow_unregulated_fixed_port_id=False,
79+
**kwargs,
7780
)
7881

7982

@@ -1986,6 +1989,22 @@ def _unittest_dsdl_parser_utf8_bytes(wrkspc: Workspace) -> None:
19861989
assert t.string_like
19871990

19881991

1992+
def _unittest_dsdl_parser_strict_mode(wrkspc: Workspace) -> None:
1993+
from pytest import raises
1994+
1995+
name = "ns/A.1.0.dsdl"
1996+
src = dedent(
1997+
r"""
1998+
byte[<=10] a
1999+
utf8[<=10] b
2000+
@sealed
2001+
"""
2002+
)
2003+
_ = parse_definition(wrkspc.parse_new(name, src), []) # success by default
2004+
with raises(InvalidDefinitionError, match="(?i).*strict.*"):
2005+
parse_definition(wrkspc.parse_new(name, src), [], strict=True)
2006+
2007+
19892008
def _unittest_dsdl_parser_expressions(wrkspc: Workspace) -> None:
19902009
from pytest import raises
19912010

setup.cfg

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ disable=
119119
too-many-public-methods,
120120
consider-using-f-string,
121121
unspecified-encoding,
122-
use-implicit-booleaness-not-comparison
122+
use-implicit-booleaness-not-comparison,
123+
too-many-arguments,
124+
too-many-locals
123125

124126
[pylint.REPORTS]
125127
output-format=colorized

0 commit comments

Comments
 (0)