diff --git a/src/mkdocstrings_handlers/python/_internal/rendering.py b/src/mkdocstrings_handlers/python/_internal/rendering.py index 8976644..a0f312e 100644 --- a/src/mkdocstrings_handlers/python/_internal/rendering.py +++ b/src/mkdocstrings_handlers/python/_internal/rendering.py @@ -805,6 +805,11 @@ def expand_identifier(self, identifier: str) -> str: if self.config.relative_crossrefs and identifier.startswith("."): # type: ignore[attr-defined] identifier = identifier[1:] obj = self.current_object + # Anchor on the docstring's parent: for inherited members the current + # object is an alias living under the inheriting class, while the + # docstring was written on the defining object in another tree. + if self.current_object.docstring is not None and self.current_object.docstring.parent is not None: + obj = self.current_object.docstring.parent while identifier and identifier[0] == ".": identifier = identifier[1:] if obj.parent is None: diff --git a/tests/test_rendering.py b/tests/test_rendering.py index 3709ec9..db1f256 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -4,12 +4,14 @@ import re from dataclasses import dataclass +from types import SimpleNamespace from typing import TYPE_CHECKING, Any, Callable, cast import pytest from griffe import Alias, ModulesCollection, Object, temporary_visited_module from mkdocstrings_handlers.python._internal import rendering +from mkdocstrings_handlers.python._internal.rendering import AutorefsHook if TYPE_CHECKING: from markupsafe import Markup @@ -174,3 +176,42 @@ def __init__(self, name: str, lineno: int | None = None, *, is_alias: bool = Fal members = [Obj("a", 10, is_alias=True), Obj("b", 9, is_alias=False), Obj("c", 8, is_alias=True)] ordered = rendering.do_order_members(members, order, members_list) # type: ignore[arg-type] assert [obj.name for obj in ordered] == expected_names + + +def test_expand_identifier_relative_crossref_in_inherited_member() -> None: + """Relative cross-references in inherited members expand against the defining object. + + The docstring of an inherited member is rendered in the docs of the inheriting + class, where the current object is an alias living under that class. Dots in + relative references must still climb the tree of the class the docstring was + written on, not the consumer's tree. + """ + collection = ModulesCollection() + with temporary_visited_module( + ''' + class Base: + MAPPING = {} + """The mapping to use for each [`Thing`][....Thing].""" + ''', + module_name="pkga", + modules_collection=collection, + ) as module: + collection["pkga"] = module + with temporary_visited_module( + """ + from pkga import Base + + class Derived(Base): ... + """, + module_name="pkgb", + modules_collection=collection, + ) as module_b: + collection["pkgb"] = module_b + config = SimpleNamespace(relative_crossrefs=True, scoped_crossrefs=False) + # Control: a non-inherited object expands relative references in its own tree. + direct = AutorefsHook(collection["pkga"]["Base"]["MAPPING"], config) # type: ignore[arg-type] + assert direct.expand_identifier("....Thing") == "pkga.Thing" + # Inherited member: the current object is an alias under pkgb.Derived, + # but the docstring was written on pkga.Base.MAPPING. + inherited = AutorefsHook(collection["pkgb"]["Derived"]["MAPPING"], config) # type: ignore[arg-type] + assert inherited.expand_identifier("....Thing") == "pkga.Thing"