From 70996f3935c326c2342469c52e3a3209c8612853 Mon Sep 17 00:00:00 2001 From: devtechedge Date: Mon, 14 Sep 2026 04:23:40 +0530 Subject: [PATCH] fix: Expand relative cross-references against the object the docstring was written on With inherited members enabled, docstrings of base class members are rendered in the docs of the inheriting class, where the current object is a Griffe alias living under the inheriting class. Leading-dot relative cross-references found in such docstrings were therefore expanded against the consumer's tree instead of the dependency's tree, resolving to wrong targets or failing strict builds. Anchor the dot-walk on the docstring's parent (the defining object) when it is reachable, falling back to the current object otherwise. --- .../python/_internal/rendering.py | 5 +++ tests/test_rendering.py | 41 +++++++++++++++++++ 2 files changed, 46 insertions(+) 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"