@@ -283,23 +283,12 @@ def dynamic_alias(
283
283
284
284
canonical_path = None
285
285
crnt_part = mod
286
+ prev_part = _NoParent
286
287
for ii , attr_name in enumerate (splits ):
288
+ # fetch attribute ----
287
289
try :
290
+ prev_part = crnt_part
288
291
crnt_part = getattr (crnt_part , attr_name )
289
- if not isinstance (crnt_part , ModuleType ) and not canonical_path :
290
- if inspect .isclass (crnt_part ) or inspect .isfunction (crnt_part ):
291
- _mod = getattr (crnt_part , "__module__" , None )
292
-
293
- if _mod is None :
294
- canonical_path = path
295
- else :
296
- canonical_path = _mod + ":" + "." .join (splits [ii :])
297
- else :
298
- canonical_path = path
299
- elif isinstance (crnt_part , ModuleType ) and ii == (len (splits ) - 1 ):
300
- # final object is module
301
- canonical_path = crnt_part .__name__
302
-
303
292
except AttributeError :
304
293
# Fetching the attribute can fail if it is purely a type hint,
305
294
# and has no value. This can be an issue if you have added a
@@ -319,6 +308,22 @@ def dynamic_alias(
319
308
f"No attribute named `{ attr_name } ` in the path `{ path } `."
320
309
)
321
310
311
+ # update canonical_path ----
312
+ # this is our belief about where the final object lives (ie. its submodule)
313
+ try :
314
+ _qualname = "." .join (splits [ii :])
315
+ _is_final = ii == (len (splits ) - 1 )
316
+ new_canonical_path = _canonical_path (
317
+ crnt_part , _qualname , _is_final , prev_part
318
+ )
319
+ except AttributeError :
320
+ new_canonical_path = None
321
+
322
+ if new_canonical_path is not None :
323
+ # Note that previously we kept the first valid canonical path,
324
+ # but now keep the last.
325
+ canonical_path = new_canonical_path
326
+
322
327
if canonical_path is None :
323
328
raise ValueError (f"Cannot find canonical path for `{ path } `" )
324
329
@@ -351,6 +356,31 @@ def dynamic_alias(
351
356
return dc .Alias (attr_name , obj , parent = parent )
352
357
353
358
359
+ class _NoParent :
360
+ """Represent the absence of a parent object."""
361
+
362
+
363
+ def _canonical_path (crnt_part : object , qualname : str , is_final = False , parent = _NoParent ):
364
+ if not isinstance (crnt_part , ModuleType ):
365
+ # classes and functions ----
366
+ if inspect .isclass (crnt_part ) or inspect .isfunction (crnt_part ):
367
+ _mod = getattr (crnt_part , "__module__" , None )
368
+
369
+ if _mod is None :
370
+ return None
371
+ else :
372
+ # we can use the object's actual __qualname__ here, which correctly
373
+ # reports the path for e.g. methods on a class
374
+ return _mod + ":" + crnt_part .__qualname__
375
+ elif parent is not _NoParent and isinstance (parent , ModuleType ):
376
+ return parent .__name__ + ":" + qualname
377
+ else :
378
+ return None
379
+ elif isinstance (crnt_part , ModuleType ) and is_final :
380
+ # final object is module
381
+ return crnt_part .__name__
382
+
383
+
354
384
def _is_valueless (obj : dc .Object ):
355
385
if isinstance (obj , dc .Attribute ):
356
386
if "class-attribute" in obj .labels and obj .value is None :
0 commit comments