@@ -45,6 +45,7 @@ class DSDLDefinition(ReadableDSDLFile):
4545 :param file_path: The path to the DSDL file.
4646 :param root_namespace_path: The path to the root namespace directory. `file_path` must be a descendant of this path.
4747 See `from_first_in` for a more flexible way to create a DSDLDefinition object.
48+ :raises InvalidDefinitionError: If file_path does not exist.
4849 """
4950
5051 @classmethod
@@ -56,20 +57,37 @@ def _infer_path_to_root_from_first_found(cls, dsdl_path: Path, valid_dsdl_roots:
5657 if valid_dsdl_roots is None :
5758 raise ValueError ("valid_dsdl_roots was None" )
5859
59- if dsdl_path .is_absolute () and len (valid_dsdl_roots ) == 0 :
60- raise PathInferenceError (
61- f"dsdl_path ({ dsdl_path } ) is absolute and the provided valid root names are empty. The DSDL root of "
62- "an absolute path cannot be inferred without this information." ,
63- dsdl_path ,
64- valid_dsdl_roots ,
65- )
66-
60+ # INFERENCE 1: The easiest inference is when the target path is relative to the current working directory and
61+ # the root is a direct child folder. In this case we allow targets to be specified as simple, relative paths
62+ # where we infer the root from the first part of each path.
6763 if len (valid_dsdl_roots ) == 0 :
68- # if we have no valid roots we can only infer the root of the path. We require the path to be relative
69- # to avoid accidental inferences given that dsdl file trees starting from a filesystem root are rare.
70- return Path (dsdl_path .parts [0 ])
71-
72- # INFERENCE 1: The strongest inference is when the path is relative to a known root.
64+ # if we have no valid roots we can only infer the root of the path.
65+ if dsdl_path .is_absolute ():
66+ # but if the path is absolute we refuse to infer the root as this would cause us to search the entire
67+ # filesystem for DSDL files and it's almost certainly wrong.
68+ raise PathInferenceError (
69+ f"No valid roots provided for absolute path { str (dsdl_path )} . Unable to infer root without "
70+ "more information." ,
71+ dsdl_path ,
72+ valid_dsdl_roots ,
73+ )
74+ else :
75+ # if the path is relative we'll assume the root is the top-most folder in the path.
76+ directly_inferred = Path (dsdl_path .parts [0 ])
77+ try :
78+ directly_inferred .resolve (strict = True )
79+ except FileNotFoundError :
80+ raise PathInferenceError (
81+ f"No valid root found in path { str (dsdl_path )} and the inferred root { str (directly_inferred )} "
82+ "does not exist. You either need to change your working directory to the folder that contains "
83+ "this root folder or provide a valid root path." ,
84+ dsdl_path ,
85+ valid_dsdl_roots ,
86+ )
87+ return directly_inferred
88+
89+ # INFERENCE 2: The next easiest inference is when the target path is relative to a known dsdl root. These
90+ # operations should work with pure paths and not require filesystem access.
7391 resolved_dsdl_path = dsdl_path .resolve (strict = False ) if dsdl_path .is_absolute () else None
7492 for path_to_root in valid_dsdl_roots :
7593 # First we try the paths as-is...
@@ -89,7 +107,27 @@ def _infer_path_to_root_from_first_found(cls, dsdl_path: Path, valid_dsdl_roots:
89107 else :
90108 return path_to_root_resolved
91109
92- # INFERENCE 2: A weaker, but valid inference is when the path is a child of a known root folder name.
110+ # INFERENCE 3: If the target is relative then we can try to find a valid root by looking for the file in the
111+ # root directories. This is a stronger inference than the previous one because it requires the file to exist
112+ # but we do it second because it reads the filesystem.
113+ if not dsdl_path .is_absolute ():
114+ for path_to_root in valid_dsdl_roots :
115+ path_to_root_parent = path_to_root
116+ while path_to_root_parent != path_to_root_parent .parent :
117+ # Weld together and check only if the root's last part is the same name as the target's first part.
118+ # yes:
119+ # path/to/root + root/then/Type.1.0.dsdl <- /root == root/
120+ # no:
121+ # path/to/not_root + root/then/Type.1.0.dsdl <- /not_root != root/
122+ if (
123+ path_to_root_parent .parts [- 1 ] == dsdl_path .parts [0 ]
124+ and (path_to_root_parent .parent / dsdl_path ).exists ()
125+ ):
126+ return path_to_root_parent
127+ path_to_root_parent = path_to_root_parent .parent
128+
129+ # INFERENCE 4: A weaker, but valid inference is when the target path is a child of a known root folder name.
130+ # This is only allowed if dsdl roots are top-level namespace names and not paths.
93131 root_parts = [x .parts [- 1 ] for x in valid_dsdl_roots if len (x .parts ) == 1 ]
94132 parts = list (dsdl_path .parent .parts )
95133 for i , part in list (enumerate (parts )):
@@ -102,24 +140,38 @@ def _infer_path_to_root_from_first_found(cls, dsdl_path: Path, valid_dsdl_roots:
102140 def from_first_in (cls : Type ["DSDLDefinition" ], dsdl_path : Path , valid_dsdl_roots : list [Path ]) -> "DSDLDefinition" :
103141 """
104142 Creates a DSDLDefinition object by inferring the path to the namespace root of a DSDL file given a set
105- of valid roots. The logic used prefers an instance of dsdl_path found to exist under a valid root but
106- will degrade to pure-path string matching if no file is found. Because this logic uses the first root path
107- that passes one of these two inferences the order of the valid_dsdl_roots list matters.
143+ of valid roots and, if the dsdl path is relative, resolving the dsdl path relative to said roots. The logic used
144+ prefers an instance of `dsdl_path` found to exist under a valid root but will degrade to pure-path string
145+ matching if no file is found (If this does not yield a valid path to an existing dsdl file an exception is
146+ raised). Because this logic uses the first root path that passes one of these two inferences the order of the
147+ valid_dsdl_roots list matters.
108148
109149 :param dsdl_path: The path to the alleged DSDL file.
110150 :param valid_dsdl_roots: The ordered set of valid root names or paths under which the type must reside.
111151 This argument is accepted as a list for ordering but no de-duplication is performed
112152 as the caller is expected to provide a correct set of paths.
113153 :return A new DSDLDefinition object
114154 :raises PathInferenceError: If the namespace root cannot be inferred from the provided information.
155+ :raises InvalidDefinitionError: If the file does not exist.
115156 """
116- return cls (dsdl_path , cls ._infer_path_to_root_from_first_found (dsdl_path , valid_dsdl_roots ))
157+ root_path = cls ._infer_path_to_root_from_first_found (dsdl_path , valid_dsdl_roots )
158+ if not dsdl_path .is_absolute ():
159+ dsdl_path_resolved = (root_path .parent / dsdl_path ).resolve (strict = False )
160+ else :
161+ dsdl_path_resolved = dsdl_path .resolve (strict = False )
162+ return cls (dsdl_path_resolved , root_path )
117163
118164 def __init__ (self , file_path : Path , root_namespace_path : Path ):
119165 """ """
120166 # Normalizing the path and reading the definition text
121167 self ._file_path = Path (file_path )
122168 del file_path
169+
170+ if not self ._file_path .exists ():
171+ raise InvalidDefinitionError (
172+ "Attempt to construct ReadableDSDLFile object for file that doesn't exist." , self ._file_path
173+ )
174+
123175 self ._root_namespace_path = Path (root_namespace_path )
124176 del root_namespace_path
125177 self ._text : str | None = None
@@ -171,8 +223,12 @@ def __init__(self, file_path: Path, root_namespace_path: Path):
171223 self ._cached_type : CompositeType | None = None
172224
173225 # +-----------------------------------------------------------------------+
174- # | ReadableDSDLFile :: INTERFACE |
226+ # | ReadableDSDLFile :: INTERFACE |
175227 # +-----------------------------------------------------------------------+
228+ @property
229+ def file_path (self ) -> Path :
230+ return self ._file_path
231+
176232 def read (
177233 self ,
178234 lookup_definitions : Iterable [ReadableDSDLFile ],
@@ -185,9 +241,6 @@ def read(
185241 _logger .debug ("%s: Cache hit" , log_prefix )
186242 return self ._cached_type
187243
188- if not self ._file_path .exists ():
189- raise InvalidDefinitionError ("Attempt to read DSDL file that doesn't exist." , self ._file_path )
190-
191244 started_at = time .monotonic ()
192245
193246 # Remove the target definition from the lookup list in order to prevent
@@ -274,10 +327,6 @@ def fixed_port_id(self) -> int | None:
274327 def has_fixed_port_id (self ) -> bool :
275328 return self .fixed_port_id is not None
276329
277- @property
278- def file_path (self ) -> Path :
279- return self ._file_path
280-
281330 @property
282331 def root_namespace_path (self ) -> Path :
283332 return self ._root_namespace_path
@@ -298,12 +347,15 @@ def __eq__(self, other: object) -> bool:
298347 return NotImplemented # pragma: no cover
299348
300349 def __str__ (self ) -> str :
301- return "DSDLDefinition(full_name=%r, version=%r, fixed_port_id=%r, file_path=%s)" % (
302- self .full_name ,
303- self .version ,
304- self .fixed_port_id ,
305- self .file_path ,
306- )
350+ try :
351+ return "DSDLDefinition(full_name=%r, version=%r, fixed_port_id=%r, file_path=%s)" % (
352+ self .full_name ,
353+ self .version ,
354+ self .fixed_port_id ,
355+ self .file_path ,
356+ )
357+ except AttributeError : # pragma: no cover
358+ return "DSDLDefinition(UNINITIALIZED)"
307359
308360 __repr__ = __str__
309361
@@ -315,13 +367,8 @@ def _unittest_dsdl_definition_read_non_existent() -> None:
315367 from pytest import raises as expect_raises
316368
317369 target = Path ("root" , "ns" , "Target.1.1.dsdl" )
318- target_definition = DSDLDefinition (target , target .parent )
319-
320- def print_output (line_number : int , text : str ) -> None : # pragma: no cover
321- _ = line_number , text
322-
323370 with expect_raises (InvalidDefinitionError ):
324- target_definition . read ([], [], print_output , True )
371+ _ = DSDLDefinition ( target , target . parent )
325372
326373
327374def _unittest_dsdl_definition_read_text (temp_dsdl_factory ) -> None : # type: ignore
@@ -354,16 +401,11 @@ def _unittest_type_from_path_inference() -> None:
354401 # case the method assumes that the relative path is the correct and complete namespace of the type:
355402
356403 # relative path
357- root = DSDLDefinition ._infer_path_to_root_from_first_found (Path ("uavcan/foo/bar/435.baz.1.0.dsdl" ), [])
404+ root = DSDLDefinition ._infer_path_to_root_from_first_found (
405+ Path ("uavcan/foo/bar/435.baz.1.0.dsdl" ), [Path ("uavcan" )]
406+ )
358407 assert root == Path ("uavcan" )
359408
360- # The root namespace is not inferred in an absolute path without additional data:
361-
362- with expect_raises (PathInferenceError ):
363- _ = DSDLDefinition ._infer_path_to_root_from_first_found (
364- Path ("/repo/uavcan/foo/bar/435.baz.1.0.dsdl" ).resolve (), []
365- )
366-
367409 with expect_raises (ValueError ):
368410 _ = DSDLDefinition ._infer_path_to_root_from_first_found (
369411 Path ("/repo/uavcan/foo/bar/435.baz.1.0.dsdl" ).resolve (), None # type: ignore
@@ -458,8 +500,31 @@ def _unittest_type_from_path_inference() -> None:
458500 assert root == Path ("repo/uavcan" )
459501
460502
461- def _unittest_from_first_in () -> None :
462- dsdl_def = DSDLDefinition .from_first_in (
463- Path ("/repo/uavcan/foo/bar/435.baz.1.0.dsdl" ).resolve (), [Path ("/repo/uavcan/foo/.." ).resolve ()]
464- )
503+ def _unittest_type_from_path_inference_edge_case (temp_dsdl_factory ) -> None : # type: ignore
504+ """
505+ Edge case where we target a file where the namespace is under the root path.
506+ """
507+ # pylint: disable=protected-access
508+
509+ from pytest import raises as expect_raises
510+ import os
511+
512+ target_path = Path ("dsdl_root/Type.1.0.dsdl" )
513+ target_file = temp_dsdl_factory .new_file (target_path , "@sealed" ).resolve ()
514+ expected_root_parent = target_file .parent .parent
515+ with expect_raises (PathInferenceError ):
516+ _ = DSDLDefinition ._infer_path_to_root_from_first_found (target_file , [])
517+
518+ old_cwd = os .getcwd ()
519+ os .chdir (expected_root_parent )
520+ try :
521+ root = DSDLDefinition ._infer_path_to_root_from_first_found (target_path , [])
522+ assert root .parent .resolve () == expected_root_parent
523+ finally :
524+ os .chdir (old_cwd )
525+
526+
527+ def _unittest_from_first_in (temp_dsdl_factory ) -> None : # type: ignore
528+ dsdl_file = temp_dsdl_factory .new_file (Path ("repo/uavcan/foo/bar/435.baz.1.0.dsdl" ), "@sealed" )
529+ dsdl_def = DSDLDefinition .from_first_in (dsdl_file .resolve (), [(dsdl_file .parent .parent / ".." )])
465530 assert dsdl_def .full_name == "uavcan.foo.bar.baz"
0 commit comments