Skip to content

Commit c0afc34

Browse files
Fix for Issue #104 (#107)
This changes our logic for detecting duplicate definitions on case-sensitive file-systems to only operate on files of interest and to fix this detection logic per issue #104. If globular searches find duplicates we do not waste time detecting this until/unless these duplicates are actually considered when parsing target definitions. --------- Co-authored-by: Pavel Kirienko <[email protected]>
1 parent c7d8ef9 commit c0afc34

File tree

4 files changed

+140
-99
lines changed

4 files changed

+140
-99
lines changed

pydsdl/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import sys as _sys
88
from pathlib import Path as _Path
99

10-
__version__ = "1.21.0"
10+
__version__ = "1.21.1"
1111
__version_info__ = tuple(map(int, __version__.split(".")[:3]))
1212
__license__ = "MIT"
1313
__author__ = "OpenCyphal"

pydsdl/_data_type_builder.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ class MissingSerializationModeError(_error.InvalidDefinitionError):
3535
pass
3636

3737

38+
class DataTypeCollisionError(_error.InvalidDefinitionError):
39+
"""
40+
Raised when there are conflicting data type definitions.
41+
"""
42+
43+
44+
class DataTypeNameCollisionError(DataTypeCollisionError):
45+
"""
46+
Raised when type collisions are caused by naming conflicts.
47+
"""
48+
49+
3850
_logger = logging.getLogger(__name__)
3951

4052

@@ -197,7 +209,11 @@ def resolve_versioned_data_type(self, name: str, version: _serializable.Version)
197209
_logger.debug("The full name of a relatively referred type %r reconstructed as %r", name, full_name)
198210

199211
del name
200-
found = list(filter(lambda d: d.full_name == full_name and d.version == version, self._lookup_definitions))
212+
found = list(
213+
filter(
214+
lambda d: d.full_name.lower() == full_name.lower() and d.version == version, self._lookup_definitions
215+
)
216+
)
201217
if not found:
202218
# Play Sherlock to help the user with mistakes like https://forum.opencyphal.org/t/904/2
203219
requested_ns = full_name.split(_serializable.CompositeType.NAME_COMPONENT_SEPARATOR)[0]
@@ -218,16 +234,34 @@ def resolve_versioned_data_type(self, name: str, version: _serializable.Version)
218234
error_description += " Please make sure that you specified the directories correctly."
219235
raise UndefinedDataTypeError(error_description)
220236

221-
if len(found) > 1: # pragma: no cover
222-
raise _error.InternalError("Conflicting definitions: %r" % found)
237+
if len(found) > 1:
238+
if (
239+
found[0].full_name != found[1].full_name and found[0].full_name.lower() == found[1].full_name.lower()
240+
): # pragma: no cover
241+
# This only happens if the file system is case-insensitive.
242+
raise DataTypeNameCollisionError(
243+
"Full name of this definition differs from %s only by letter case, "
244+
"which is not permitted" % found[0].file_path,
245+
path=found[1].file_path,
246+
)
247+
raise DataTypeCollisionError("Conflicting definitions: %r" % found)
248+
elif found[0].full_name != full_name and found[0].full_name.lower() == full_name.lower():
249+
# pragma: no cover
250+
# This only happens if the file system is case-sensitive.
251+
raise DataTypeNameCollisionError(
252+
"Full name of required definition %s differs from %s only by letter case, "
253+
"which is not permitted" % (full_name, found[0].full_name),
254+
path=found[0].file_path,
255+
)
223256

224257
target_definition = found[0]
225-
for visitor in self._definition_visitors:
226-
visitor.on_definition(self._definition, target_definition)
227258

228259
assert isinstance(target_definition, ReadableDSDLFile)
229-
assert target_definition.full_name == full_name
230260
assert target_definition.version == version
261+
262+
for visitor in self._definition_visitors:
263+
visitor.on_definition(self._definition, target_definition)
264+
231265
# Recursion is cool.
232266
dt = target_definition.read(
233267
lookup_definitions=self._lookup_definitions,

pydsdl/_namespace.py

Lines changed: 26 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,6 @@ class RootNamespaceNameCollisionError(_error.InvalidDefinitionError):
2727
"""
2828

2929

30-
class DataTypeCollisionError(_error.InvalidDefinitionError):
31-
"""
32-
Raised when there are conflicting data type definitions.
33-
"""
34-
35-
36-
class DataTypeNameCollisionError(DataTypeCollisionError):
37-
"""
38-
Raised when there are conflicting data type names.
39-
"""
40-
41-
4230
class NestedRootNamespaceError(_error.InvalidDefinitionError):
4331
"""
4432
Nested root namespaces are not allowed. This exception is thrown when this rule is violated.
@@ -260,9 +248,6 @@ def _complete_read_function(
260248

261249
lookup_dsdl_definitions = _construct_dsdl_definitions_from_namespaces(lookup_directories_path_list)
262250

263-
# Check for collisions against the lookup definitions also.
264-
_ensure_no_collisions(target_dsdl_definitions, lookup_dsdl_definitions)
265-
266251
_logger.debug("Lookup DSDL definitions are listed below:")
267252
for x in lookup_dsdl_definitions:
268253
_logger.debug(_LOG_LIST_ITEM_PREFIX + str(x))
@@ -386,44 +371,6 @@ def _construct_dsdl_definitions_from_namespaces(
386371
return dsdl_file_sort([_dsdl_definition.DSDLDefinition(*p) for p in source_file_paths])
387372

388373

389-
def _ensure_no_collisions(
390-
target_definitions: list[ReadableDSDLFile],
391-
lookup_definitions: list[ReadableDSDLFile],
392-
) -> None:
393-
for tg in target_definitions:
394-
tg_full_namespace_period = tg.full_namespace.lower() + "."
395-
tg_full_name_period = tg.full_name.lower() + "."
396-
for lu in lookup_definitions:
397-
lu_full_namespace_period = lu.full_namespace.lower() + "."
398-
lu_full_name_period = lu.full_name.lower() + "."
399-
# This is to allow the following messages to coexist happily:
400-
# zubax/non_colliding/iceberg/Ice.0.1.dsdl
401-
# zubax/non_colliding/IceB.0.1.dsdl
402-
# The following is still not allowed:
403-
# zubax/colliding/iceberg/Ice.0.1.dsdl
404-
# zubax/colliding/Iceberg.0.1.dsdl
405-
if tg.full_name != lu.full_name and tg.full_name.lower() == lu.full_name.lower():
406-
raise DataTypeNameCollisionError(
407-
"Full name of this definition differs from %s only by letter case, "
408-
"which is not permitted" % lu.file_path,
409-
path=tg.file_path,
410-
)
411-
if (tg_full_namespace_period).startswith(lu_full_name_period):
412-
raise DataTypeNameCollisionError(
413-
"The namespace of this type conflicts with %s" % lu.file_path, path=tg.file_path
414-
)
415-
if (lu_full_namespace_period).startswith(tg_full_name_period):
416-
raise DataTypeNameCollisionError(
417-
"This type conflicts with the namespace of %s" % lu.file_path, path=tg.file_path
418-
)
419-
if (
420-
tg_full_name_period == lu_full_name_period
421-
and tg.version == lu.version
422-
and not tg.file_path.samefile(lu.file_path)
423-
): # https://github.com/OpenCyphal/pydsdl/issues/94
424-
raise DataTypeCollisionError("This type is redefined in %s" % lu.file_path, path=tg.file_path)
425-
426-
427374
def _ensure_no_fixed_port_id_collisions(types: list[_serializable.CompositeType]) -> None:
428375
for a in types:
429376
for b in types:
@@ -864,22 +811,33 @@ def _unittest_read_files_empty_args() -> None:
864811
assert len(transitive) == 0
865812

866813

867-
def _unittest_ensure_no_collisions(temp_dsdl_factory) -> None: # type: ignore
868-
from pytest import raises as expect_raises
814+
def _unittest_ensure_no_namespace_name_collisions_or_nested_root_namespaces() -> None:
815+
"""gratuitous coverage of the collision check where other tests don't cover some edge cases."""
816+
_ensure_no_namespace_name_collisions_or_nested_root_namespaces([], False)
869817

870-
_ = temp_dsdl_factory
871818

872-
# gratuitous coverage of the collision check where other tests don't cover some edge cases
873-
_ensure_no_namespace_name_collisions_or_nested_root_namespaces([], False)
819+
def _unittest_issue_104(temp_dsdl_factory) -> None: # type: ignore
820+
"""demonstrate that removing _ensure_no_collisions is okay"""
874821

875-
with expect_raises(DataTypeNameCollisionError):
876-
_ensure_no_collisions(
877-
[_dsdl_definition.DSDLDefinition(Path("a/b.1.0.dsdl"), Path("a"))],
878-
[_dsdl_definition.DSDLDefinition(Path("a/B.1.0.dsdl"), Path("a"))],
879-
)
822+
from pytest import raises
880823

881-
with expect_raises(DataTypeNameCollisionError):
882-
_ensure_no_collisions(
883-
[_dsdl_definition.DSDLDefinition(Path("a/b/c.1.0.dsdl"), Path("a"))],
884-
[_dsdl_definition.DSDLDefinition(Path("a/b.1.0.dsdl"), Path("a"))],
885-
)
824+
thing_1_0 = Path("a/b/thing.1.0.dsdl")
825+
thing_type_1_0 = Path("a/b/thing/thingtype.1.0.dsdl")
826+
827+
file_at_root = temp_dsdl_factory.new_file(Path("a/Nothing.1.0.dsdl"), "@sealed\n")
828+
thing_file = temp_dsdl_factory.new_file(thing_1_0, "@sealed\na.b.thing.thingtype.1.0 thing\n")
829+
_ = temp_dsdl_factory.new_file(thing_type_1_0, "@sealed\n")
830+
831+
direct, transitive = read_files(thing_file, file_at_root.parent, file_at_root.parent)
832+
833+
assert len(direct) == 1
834+
assert len(transitive) == 1
835+
836+
thing_1_1 = Path("a/b/thing.1.1.dsdl")
837+
838+
thing_file2 = temp_dsdl_factory.new_file(thing_1_1, "@sealed\na.b.thing.Thingtype.1.0 thing\n")
839+
840+
from ._data_type_builder import DataTypeNameCollisionError
841+
842+
with raises(DataTypeNameCollisionError):
843+
read_files(thing_file2, file_at_root.parent, file_at_root.parent)

pydsdl/_test.py

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
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
1415
from . import _expression
1516
from . import _error
1617
from . import _parser
@@ -25,6 +26,8 @@
2526
class Workspace:
2627
def __init__(self) -> None:
2728
self._tmp_dir = tempfile.TemporaryDirectory(prefix="pydsdl-test-")
29+
name = self._tmp_dir.name.upper()
30+
self._is_case_sensitive = not Path(name).exists()
2831

2932
@property
3033
def directory(self) -> Path:
@@ -58,6 +61,10 @@ def drop(self, rel_path_glob: str) -> None:
5861
for g in self.directory.glob(rel_path_glob):
5962
g.unlink()
6063

64+
@property
65+
def is_fs_case_sensitive(self) -> bool:
66+
return self._is_case_sensitive
67+
6168

6269
def parse_definition(
6370
definition: _dsdl_definition.DSDLDefinition, lookup_definitions: Sequence[_dsdl_definition.DSDLDefinition]
@@ -1096,7 +1103,7 @@ def print_handler(d: Path, line: int, text: str) -> None:
10961103
"""
10971104
),
10981105
)
1099-
with raises(_namespace.DataTypeNameCollisionError):
1106+
with raises(_namespace.FixedPortIDCollisionError):
11001107
_namespace.read_namespace(
11011108
wrkspc.directory / "zubax",
11021109
[
@@ -1105,30 +1112,27 @@ def print_handler(d: Path, line: int, text: str) -> None:
11051112
)
11061113

11071114
# Do again to test single lookup-directory override
1108-
with raises(_namespace.DataTypeNameCollisionError):
1115+
with raises(_namespace.FixedPortIDCollisionError):
11091116
_namespace.read_namespace(wrkspc.directory / "zubax", wrkspc.directory / "zubax")
11101117

1111-
try:
1112-
(wrkspc.directory / "zubax/colliding/iceberg/300.Ice.30.0.dsdl").unlink()
1113-
wrkspc.new(
1114-
"zubax/COLLIDING/300.Iceberg.30.0.dsdl",
1115-
dedent(
1116-
"""
1117-
@extent 1024
1118-
---
1119-
@extent 1024
1118+
(wrkspc.directory / "zubax/colliding/iceberg/300.Ice.30.0.dsdl").unlink()
1119+
wrkspc.new(
1120+
"zubax/COLLIDING/300.Iceberg.30.0.dsdl",
1121+
dedent(
11201122
"""
1121-
),
1122-
)
1123-
with raises(_namespace.DataTypeNameCollisionError, match=".*letter case.*"):
1124-
_namespace.read_namespace(
1123+
@extent 1024
1124+
---
1125+
@extent 1024
1126+
"""
1127+
),
1128+
)
1129+
with raises(_namespace.FixedPortIDCollisionError):
1130+
_namespace.read_namespace(
1131+
wrkspc.directory / "zubax",
1132+
[
11251133
wrkspc.directory / "zubax",
1126-
[
1127-
wrkspc.directory / "zubax",
1128-
],
1129-
)
1130-
except _namespace.FixedPortIDCollisionError: # pragma: no cover
1131-
pass # We're running on a platform where paths are not case-sensitive.
1134+
],
1135+
)
11321136

11331137
# Test namespace can intersect with type name
11341138
(wrkspc.directory / "zubax/COLLIDING/300.Iceberg.30.0.dsdl").unlink()
@@ -1161,6 +1165,52 @@ def print_handler(d: Path, line: int, text: str) -> None:
11611165
assert "zubax.noncolliding.Iceb" in [x.full_name for x in parsed]
11621166

11631167

1168+
def _unittest_collision_on_case_sensitive_filesystem(wrkspc: Workspace) -> None:
1169+
from pytest import raises
1170+
1171+
if not wrkspc.is_fs_case_sensitive: # pragma: no cover
1172+
pytest.skip("This test is only relevant on case-sensitive filesystems.")
1173+
1174+
# Empty namespace.
1175+
assert [] == _namespace.read_namespace(wrkspc.directory)
1176+
1177+
wrkspc.new(
1178+
"atlantic/ships/Titanic.1.0.dsdl",
1179+
dedent(
1180+
"""
1181+
greenland.colliding.IceBerg.1.0[<=2] bergs
1182+
@sealed
1183+
"""
1184+
),
1185+
)
1186+
1187+
wrkspc.new(
1188+
"greenland/colliding/IceBerg.1.0.dsdl",
1189+
dedent(
1190+
"""
1191+
@sealed
1192+
"""
1193+
),
1194+
)
1195+
1196+
wrkspc.new(
1197+
"greenland/COLLIDING/IceBerg.1.0.dsdl",
1198+
dedent(
1199+
"""
1200+
@sealed
1201+
"""
1202+
),
1203+
)
1204+
1205+
with raises(_data_type_builder.DataTypeNameCollisionError, match=".*letter case.*"):
1206+
_namespace.read_namespace(
1207+
wrkspc.directory / "atlantic",
1208+
[
1209+
wrkspc.directory / "greenland",
1210+
],
1211+
)
1212+
1213+
11641214
def _unittest_parse_namespace_versioning(wrkspc: Workspace) -> None:
11651215
from pytest import raises
11661216

@@ -1265,8 +1315,7 @@ def _unittest_parse_namespace_versioning(wrkspc: Workspace) -> None:
12651315
),
12661316
)
12671317

1268-
with raises(_namespace.DataTypeCollisionError):
1269-
_namespace.read_namespace((wrkspc.directory / "ns"), [])
1318+
_namespace.read_namespace((wrkspc.directory / "ns"), [])
12701319

12711320
wrkspc.drop("ns/Spartans.30.2.dsdl")
12721321

@@ -1614,7 +1663,7 @@ def _unittest_issue94(wrkspc: Workspace) -> None:
16141663
wrkspc.new("outer_b/ns/Foo.1.0.dsdl", "@sealed") # Conflict!
16151664
wrkspc.new("outer_a/ns/Bar.1.0.dsdl", "Foo.1.0 fo\n@sealed") # Which Foo.1.0?
16161665

1617-
with raises(_namespace.DataTypeCollisionError):
1666+
with raises(_data_type_builder.DataTypeCollisionError):
16181667
_namespace.read_namespace(
16191668
wrkspc.directory / "outer_a" / "ns",
16201669
[wrkspc.directory / "outer_b" / "ns"],

0 commit comments

Comments
 (0)