diff --git a/CHANGELOG.md b/CHANGELOG.md index a7ddb80..cd500d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ *Note that versions roughly correspond to the version of mkdocstrings-python that they are compatible with.* +## 1.16.4 + +* Fix handling of aliases (see bug #47) + ## 1.16.3 * Added `check_crossrefs_exclude` config option diff --git a/pixi.lock b/pixi.lock index b1f48b2..fc18bd0 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1738,8 +1738,8 @@ packages: timestamp: 1748965218001 - pypi: ./ name: mkdocstrings-python-xref - version: 1.16.3 - sha256: 207356af5ea06c13597d5d38455eaa328f5aaba629b2410ff00f85d3d17cd976 + version: 1.16.4 + sha256: fcb8e760b689ad912bcffadf901d9984a3505913fd40b332b764877724e4d3a2 requires_dist: - griffe>=1.0 - mkdocstrings-python>=1.16.6,<2.0 diff --git a/pyproject.toml b/pyproject.toml index 9dc20ac..cfc9b73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dynamic = ["version"] requires-python = ">=3.9" dependencies = [ "mkdocstrings-python >=1.16.6,<2.0", - "griffe >=1.0" + "griffe >=1.0", ] [project.urls] @@ -47,7 +47,7 @@ dev = [ "mike >=1.1", "mkdocs >=1.5.3,<2.0", "mkdocs-material >=9.5.4", - "linkchecker >=10.4" + "linkchecker >=10.4", ] [tool.pixi.workspace] diff --git a/src/mkdocstrings_handlers/python_xref/VERSION b/src/mkdocstrings_handlers/python_xref/VERSION index c807441..a232073 100644 --- a/src/mkdocstrings_handlers/python_xref/VERSION +++ b/src/mkdocstrings_handlers/python_xref/VERSION @@ -1 +1 @@ -1.16.3 +1.16.4 diff --git a/src/mkdocstrings_handlers/python_xref/crossref.py b/src/mkdocstrings_handlers/python_xref/crossref.py index 2fb3084..57ed60e 100644 --- a/src/mkdocstrings_handlers/python_xref/crossref.py +++ b/src/mkdocstrings_handlers/python_xref/crossref.py @@ -20,7 +20,7 @@ import sys from typing import Any, Callable, List, Optional, cast -from griffe import Docstring, Object +from griffe import Alias, Docstring, GriffeError, Object from mkdocstrings import get_logger __all__ = [ @@ -318,7 +318,10 @@ def _error(self, msg: str, just_warn: bool = False) -> None: self._ok = just_warn -def substitute_relative_crossrefs(obj: Object, checkref: Optional[Callable[[str], bool]] = None) -> None: +def substitute_relative_crossrefs( + obj: Alias|Object, + checkref: Optional[Callable[[str], bool]] = None, +) -> None: """Recursively expand relative cross-references in all docstrings in tree. Arguments: @@ -326,13 +329,21 @@ def substitute_relative_crossrefs(obj: Object, checkref: Optional[Callable[[str] checkref: optional function to check whether computed cross-reference is valid. Should return True if valid, False if not valid. """ + if isinstance(obj, Alias): + try: + obj = obj.target + except GriffeError: + # If alias could not be resolved, it probably refers + # to an external package, not be documented. + return + doc = obj.docstring if doc is not None: doc.value = _RE_CROSSREF.sub(_RelativeCrossrefProcessor(doc, checkref=checkref), doc.value) for member in obj.members.values(): - if isinstance(member, Object): # pragma: no branch + if isinstance(member, (Alias,Object)): # pragma: no branch substitute_relative_crossrefs(member, checkref=checkref) def doc_value_offset_to_location(doc: Docstring, offset: int) -> tuple[int,int]: diff --git a/tests/project/src/myproj/bar.py b/tests/project/src/myproj/bar.py index 9bb80b7..8f85fc2 100644 --- a/tests/project/src/myproj/bar.py +++ b/tests/project/src/myproj/bar.py @@ -21,6 +21,11 @@ class Bar(Foo): """See [bar][.] method.""" + attribute: str = "attribute" + """ + See [`foo`][(c).] + """ + def bar(self) -> None: """This is in the [Bar][(c)] class. Also see the [foo][^.] method and the [func][(m).] function. diff --git a/tests/project/src/myproj/pkg/__init__.py b/tests/project/src/myproj/pkg/__init__.py index 9e30385..e18a461 100644 --- a/tests/project/src/myproj/pkg/__init__.py +++ b/tests/project/src/myproj/pkg/__init__.py @@ -15,6 +15,13 @@ A module """ +from .dataclass import Dataclass + +__all__ = [ + "Dataclass", + "func", +] + def func() -> None: """ A function diff --git a/tests/project/src/myproj/pkg/dataclass.py b/tests/project/src/myproj/pkg/dataclass.py new file mode 100644 index 0000000..2ef7dce --- /dev/null +++ b/tests/project/src/myproj/pkg/dataclass.py @@ -0,0 +1,25 @@ +""" +Dataclass example +""" + +from dataclasses import dataclass, field + +@dataclass +class Dataclass: + """ + Test dataclasses + + See [content][(c).] for an example attribute. + + See [method][(c).] + """ + content: str = "hi" + """some content""" + + duration: float = field(default_factory=lambda: 0.0) + """ + example: [`content`][(c).] + """ + + def method(self) -> None: + """Example method.""" diff --git a/tests/test_crossref.py b/tests/test_crossref.py index 501ef0e..fa3a768 100644 --- a/tests/test_crossref.py +++ b/tests/test_crossref.py @@ -23,6 +23,7 @@ from textwrap import dedent from typing import Callable, Optional +import griffe import pytest from griffe import Class, Docstring, Function, Module, Object, LinesCollection @@ -264,3 +265,18 @@ def test_doc_value_offset_to_location() -> None: assert doc_value_offset_to_location(doc3, 0) == (2, 5) assert doc_value_offset_to_location(doc3, 6) == (3, 3) + +def test_griffe() -> None: + """ + Test substitution on griffe rep of local project + Returns: + + """ + this_dir = Path(__file__).parent + test_src_dir = this_dir / "project" / "src" + myproj = griffe.load( + "myproj", + search_paths = [ test_src_dir ], + ) + substitute_relative_crossrefs(myproj) + # TODO - grovel output diff --git a/tests/test_integration.py b/tests/test_integration.py index 42661ac..db04c42 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -36,7 +36,7 @@ def check_autorefs(autorefs: List[Any], cases: Dict[Tuple[str,str],str] ) -> Non Arguments: autorefs: list of autoref tags parsed from HTML cases: mapping from (,) to generated reference tag - where <location? is the qualified name of the object whose doc string + where <location> is the qualified name of the object whose doc string contains the cross-reference, and <title> is the text in the cross-reference. """ cases = cases.copy() @@ -123,4 +123,17 @@ def test_integration(tmpdir: PathLike) -> None: } ) + pkg_html = site_dir.joinpath('pkg', 'index.html').read_text() + pkg_bs = bs4.BeautifulSoup(pkg_html, 'html.parser') + autorefs = pkg_bs.find_all('a', attrs={'class':'autorefs'}) + assert len(autorefs) >= 3 + + check_autorefs( + autorefs, + { + ('myproj.pkg.Dataclass', 'content') : '#myproj.pkg.Dataclass.content', + ('myproj.pkg.Dataclass', 'method') : '#myproj.pkg.Dataclass.method', + ('myproj.pkg.Dataclass.duration', 'content') : '#myproj.pkg.Dataclass.content', + } + )