Skip to content

Commit 3881be4

Browse files
authored
Merge pull request #305 from machow/fix-dynamic-canonical-path
fix: get_object dynamic supports methods set by assignments
2 parents 540d29c + cd7fd5a commit 3881be4

File tree

3 files changed

+63
-14
lines changed

3 files changed

+63
-14
lines changed

quartodoc/autosummary.py

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -283,23 +283,12 @@ def dynamic_alias(
283283

284284
canonical_path = None
285285
crnt_part = mod
286+
prev_part = _NoParent
286287
for ii, attr_name in enumerate(splits):
288+
# fetch attribute ----
287289
try:
290+
prev_part = crnt_part
288291
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-
303292
except AttributeError:
304293
# Fetching the attribute can fail if it is purely a type hint,
305294
# and has no value. This can be an issue if you have added a
@@ -319,6 +308,22 @@ def dynamic_alias(
319308
f"No attribute named `{attr_name}` in the path `{path}`."
320309
)
321310

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+
322327
if canonical_path is None:
323328
raise ValueError(f"Cannot find canonical path for `{path}`")
324329

@@ -351,6 +356,31 @@ def dynamic_alias(
351356
return dc.Alias(attr_name, obj, parent=parent)
352357

353358

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+
354384
def _is_valueless(obj: dc.Object):
355385
if isinstance(obj, dc.Attribute):
356386
if "class-attribute" in obj.labels and obj.value is None:

quartodoc/tests/example_alias_target.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@
66

77
def alias_target():
88
"""An alias target"""
9+
10+
11+
class AClass:
12+
some_method = nested_alias_target

quartodoc/tests/test_basic.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,18 @@ def test_get_object_dynamic_module_attr_class_instance():
114114

115115
assert obj.path == "quartodoc.tests.example_dynamic.some_instance"
116116
assert obj.docstring.value == "Dynamic instance doc"
117+
118+
119+
def test_get_object_dynamic_class_method_assigned():
120+
# method is assigned to class using
121+
# some_method = some_function
122+
obj = get_object(
123+
"quartodoc.tests.example_alias_target:AClass.some_method", dynamic=True
124+
)
125+
126+
assert isinstance(obj, dc.Alias)
127+
assert isinstance(obj.target, dc.Function)
128+
assert (
129+
obj.target.path
130+
== "quartodoc.tests.example_alias_target__nested.nested_alias_target"
131+
)

0 commit comments

Comments
 (0)