From e15dd3d319109fbc34336448e6f5a8a538e2aacd Mon Sep 17 00:00:00 2001 From: Jean Hominal Date: Thu, 20 Mar 2025 23:08:10 +0100 Subject: [PATCH 1/5] docs: Fix example usage for inventories PR-265: https://github.com/mkdocstrings/python/pull/265 --- docs/usage/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/usage/index.md b/docs/usage/index.md index 84110936..b2a00955 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -87,8 +87,8 @@ plugins: - mkdocstrings: handlers: python: - import: - - https://docs.python-requests.org/en/master/objects.inv + inventories: + - https://docs.python.org/3/objects.inv ``` When loading an inventory, you enable automatic cross-references From 681afb146225d98350a8eb2178aab07aec95fe6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 21 Mar 2025 18:52:38 +0100 Subject: [PATCH 2/5] refactor: Sort objects without line numbers last instead of first --- .../python/_internal/rendering.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/mkdocstrings_handlers/python/_internal/rendering.py b/src/mkdocstrings_handlers/python/_internal/rendering.py index 8ab9b21f..c762b2f8 100644 --- a/src/mkdocstrings_handlers/python/_internal/rendering.py +++ b/src/mkdocstrings_handlers/python/_internal/rendering.py @@ -43,18 +43,17 @@ _logger = get_logger(__name__) -def _sort_key_alphabetical(item: CollectorItem) -> Any: - # chr(sys.maxunicode) is a string that contains the final unicode - # character, so if 'name' isn't found on the object, the item will go to - # the end of the list. +def _sort_key_alphabetical(item: CollectorItem) -> str: + # `chr(sys.maxunicode)` is a string that contains the final unicode character, + # so if `name` isn't found on the object, the item will go to the end of the list. return item.name or chr(sys.maxunicode) -def _sort_key_source(item: CollectorItem) -> Any: - # if 'lineno' is none, the item will go to the start of the list. +def _sort_key_source(item: CollectorItem) -> float: + # If `lineno` is none, the item will go to the end of the list. if item.is_alias: - return item.alias_lineno if item.alias_lineno is not None else -1 - return item.lineno if item.lineno is not None else -1 + return item.alias_lineno if item.alias_lineno is not None else float("inf") + return item.lineno if item.lineno is not None else float("inf") Order = Literal["alphabetical", "source"] From bfb5b303f4ea2187c15bccc688f7eba25e7edfcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 22 Mar 2025 13:28:48 +0100 Subject: [PATCH 3/5] refactor: Prepare feature for ordering by `__all__` value Issue-219: https://github.com/mkdocstrings/python/issues/219 --- docs/insiders/changelog.md | 4 +++ docs/insiders/goals.yml | 3 +++ docs/usage/configuration/members.md | 13 +++++++--- pyproject.toml | 2 +- .../python/_internal/config.py | 11 ++++++-- .../python/_internal/rendering.py | 26 +++++++++++++++---- 6 files changed, 48 insertions(+), 11 deletions(-) diff --git a/docs/insiders/changelog.md b/docs/insiders/changelog.md index edddd290..b5717892 100644 --- a/docs/insiders/changelog.md +++ b/docs/insiders/changelog.md @@ -2,6 +2,10 @@ ## mkdocstrings-python Insiders +### 1.12.0 March 22, 2025 { id="1.12.0" } + +- [Ordering method: `__all__`][option-members_order] + ### 1.11.0 March 20, 2025 { id="1.11.0" } - [Filtering method: `public`][option-filters-public] diff --git a/docs/insiders/goals.yml b/docs/insiders/goals.yml index 1dbf1597..71128361 100644 --- a/docs/insiders/goals.yml +++ b/docs/insiders/goals.yml @@ -48,3 +48,6 @@ goals: - name: "Filtering method: `public`" ref: /usage/configuration/members/#option-filters-public since: 2025/03/20 + - name: "Ordering method: `__all__`" + ref: /usage/configuration/members/#option-members_order + since: 2025/03/22 \ No newline at end of file diff --git a/docs/usage/configuration/members.md b/docs/usage/configuration/members.md index 52e0d574..7a5069a1 100644 --- a/docs/usage/configuration/members.md +++ b/docs/usage/configuration/members.md @@ -264,13 +264,14 @@ class Main(Base): [](){#option-members_order} ## `members_order` -- **:octicons-package-24: Type [`str`][] :material-equal: `"alphabetical"`{ title="default value" }** +- **:octicons-package-24: Type `str | list[str]` :material-equal: `"alphabetical"`{ title="default value" }** The members ordering to use. Possible values: -- `alphabetical`: order by the members names. -- `source`: order members as they appear in the source file. +- `__all__` ([:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } — [:octicons-tag-24: Insiders 1.12.0](../../insiders/changelog.md#1.12.0)): Order according to `__all__` attributes. Since classes do not define `__all__` attributes, you can specify a second ordering method by using a list. +- `alphabetical`: Order by the members names. +- `source`: Order members as they appear in the source file. The order applies for all members, recursively. The order will be ignored for members that are explicitely sorted using the [`members`][] option. @@ -292,6 +293,12 @@ plugins: members_order: source ``` +```md title="or in docs/some_page.md (local configuration)" +::: package.module + options: + members_order: [__all__, source] +``` + ```python title="package/module.py" """Module docstring.""" diff --git a/pyproject.toml b/pyproject.toml index efaf7c5c..3ec25ccc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ classifiers = [ dependencies = [ "mkdocstrings>=0.28.3", "mkdocs-autorefs>=1.4", - "griffe>=0.49", + "griffe>=1.6.2", "typing-extensions>=4.0; python_version < '3.11'", ] diff --git a/src/mkdocstrings_handlers/python/_internal/config.py b/src/mkdocstrings_handlers/python/_internal/config.py index 89ea451c..11c77da1 100644 --- a/src/mkdocstrings_handlers/python/_internal/config.py +++ b/src/mkdocstrings_handlers/python/_internal/config.py @@ -9,6 +9,8 @@ from mkdocstrings import get_logger +from mkdocstrings_handlers.python._internal.rendering import Order # noqa: TC001 + # YORE: EOL 3.10: Replace block with line 2. if sys.version_info >= (3, 11): from typing import Self @@ -520,13 +522,18 @@ class PythonInputOptions: ] = None members_order: Annotated[ - Literal["alphabetical", "source"], + Order | list[Order], _Field( group="members", description="""The members ordering to use. - - `alphabetical`: order by the members names, + - `__all__`: order members according to `__all__` module attributes, if declared; + - `alphabetical`: order members alphabetically; - `source`: order members as they appear in the source file. + + Since `__all__` is a module-only attribute, it can't be used to sort class members, + therefore the `members_order` option accepts a list of ordering methods, + indicating ordering preferences. """, ), ] = "alphabetical" diff --git a/src/mkdocstrings_handlers/python/_internal/rendering.py b/src/mkdocstrings_handlers/python/_internal/rendering.py index c762b2f8..6725b314 100644 --- a/src/mkdocstrings_handlers/python/_internal/rendering.py +++ b/src/mkdocstrings_handlers/python/_internal/rendering.py @@ -9,6 +9,7 @@ import sys import warnings from collections import defaultdict +from contextlib import suppress from dataclasses import replace from functools import lru_cache from pathlib import Path @@ -56,12 +57,22 @@ def _sort_key_source(item: CollectorItem) -> float: return item.lineno if item.lineno is not None else float("inf") -Order = Literal["alphabetical", "source"] -"""Ordering methods.""" +def _sort__all__(item: CollectorItem) -> float: # noqa: ARG001 + raise ValueError("Not implemented in public version of mkdocstrings-python") -_order_map = { + +Order = Literal["__all__", "alphabetical", "source"] +"""Ordering methods. + +- `__all__`: order members according to `__all__` module attributes, if declared; +- `alphabetical`: order members alphabetically; +- `source`: order members as they appear in the source file. +""" + +_order_map: dict[str, Callable[[Object | Alias], str | float]] = { "alphabetical": _sort_key_alphabetical, "source": _sort_key_source, + "__all__": _sort__all__, } @@ -245,7 +256,7 @@ def do_format_attribute( def do_order_members( members: Sequence[Object | Alias], - order: Order, + order: Order | list[Order], members_list: bool | list[str] | None, ) -> Sequence[Object | Alias]: """Order members given an ordering method. @@ -265,7 +276,12 @@ def do_order_members( if name in members_dict: sorted_members.append(members_dict[name]) return sorted_members - return sorted(members, key=_order_map[order]) + if isinstance(order, str): + order = [order] + for method in order: + with suppress(ValueError): + return sorted(members, key=_order_map[method]) + return members # YORE: Bump 2: Remove block. From f3917e9dd50ca7f94d0dd22b6e4e11885b4617e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 22 Mar 2025 15:09:28 +0100 Subject: [PATCH 4/5] fix: Prevent infinite recursion by detecting parent-member cycles Issue-griffe-368: https://github.com/mkdocstrings/griffe/issues/368 --- .../python/_internal/rendering.py | 35 +++++++++++++++++-- tests/test_handler.py | 25 ++++++++++++- tests/test_rendering.py | 2 ++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/mkdocstrings_handlers/python/_internal/rendering.py b/src/mkdocstrings_handlers/python/_internal/rendering.py index 6725b314..8f544014 100644 --- a/src/mkdocstrings_handlers/python/_internal/rendering.py +++ b/src/mkdocstrings_handlers/python/_internal/rendering.py @@ -18,6 +18,8 @@ from griffe import ( Alias, + AliasResolutionError, + CyclicAliasError, DocstringAttribute, DocstringClass, DocstringFunction, @@ -411,6 +413,29 @@ def _keep_object(name: str, filters: Sequence[tuple[Pattern, bool]]) -> bool: return keep +def _parents(obj: Alias) -> set[str]: + parent: Object | Alias = obj.parent # type: ignore[assignment] + parents = {obj.path, parent.path} + if parent.is_alias: + parents.add(parent.final_target.path) # type: ignore[union-attr] + while parent.parent: + parent = parent.parent + parents.add(parent.path) + if parent.is_alias: + parents.add(parent.final_target.path) # type: ignore[union-attr] + return parents + + +def _remove_cycles(objects: list[Object | Alias]) -> Iterator[Object | Alias]: + suppress_errors = suppress(AliasResolutionError, CyclicAliasError) + for obj in objects: + if obj.is_alias: + with suppress_errors: + if obj.final_target.path in _parents(obj): # type: ignore[arg-type,union-attr] + continue + yield obj + + def do_filter_objects( objects_dictionary: dict[str, Object | Alias], *, @@ -470,10 +495,14 @@ def do_filter_objects( objects = [ obj for obj in objects if _keep_object(obj.name, filters) or (inherited_members_specified and obj.inherited) ] - if keep_no_docstrings: - return objects + if not keep_no_docstrings: + objects = [obj for obj in objects if obj.has_docstrings or (inherited_members_specified and obj.inherited)] + + # Prevent infinite recursion. + if objects: + objects = list(_remove_cycles(objects)) - return [obj for obj in objects if obj.has_docstrings or (inherited_members_specified and obj.inherited)] + return objects @lru_cache(maxsize=1) diff --git a/tests/test_handler.py b/tests/test_handler.py index a4e1d23a..5940af5e 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -10,7 +10,14 @@ from typing import TYPE_CHECKING import pytest -from griffe import Docstring, DocstringSectionExamples, DocstringSectionKind, Module, temporary_visited_module +from griffe import ( + Docstring, + DocstringSectionExamples, + DocstringSectionKind, + Module, + temporary_inspected_module, + temporary_visited_module, +) from mkdocstrings import CollectionError from mkdocstrings_handlers.python import PythonConfig, PythonHandler, PythonOptions @@ -275,3 +282,19 @@ def test_deduplicate_summary_sections(handler: PythonHandler, section: str, code ), ) assert html.count(f"{section}:") == 1 + + +def test_inheriting_self_from_parent_class(handler: PythonHandler) -> None: + """Inspect self only once when inheriting it from parent class.""" + with temporary_inspected_module( + """ + class A: ... + class B(A): ... + A.B = B + """, + ) as module: + # Assert no recusrion error. + handler.render( + module, + handler.get_options({"inherited_members": True}), + ) diff --git a/tests/test_rendering.py b/tests/test_rendering.py index 31829e85..2616610f 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -58,6 +58,8 @@ def test_format_signature(name: Markup, signature: str) -> None: class _FakeObject: name: str inherited: bool = False + parent: None = None + is_alias: bool = False @pytest.mark.parametrize( From ecc5fe1d7df6056302c32b8843cf6cbb3866e8b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 24 Mar 2025 12:24:38 +0100 Subject: [PATCH 5/5] chore: Prepare release 1.16.8 --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67ae3c02..cf5d3741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.16.8](https://github.com/mkdocstrings/python/releases/tag/1.16.8) - 2025-03-24 + +[Compare with 1.16.7](https://github.com/mkdocstrings/python/compare/1.16.7...1.16.8) + +### Bug Fixes + +- Prevent infinite recursion by detecting parent-member cycles ([f3917e9](https://github.com/mkdocstrings/python/commit/f3917e9dd50ca7f94d0dd22b6e4e11885b4617e7) by Timothée Mazzucotelli). [Issue-griffe-368](https://github.com/mkdocstrings/griffe/issues/368) + +### Code Refactoring + +- Prepare feature for ordering by `__all__` value ([bfb5b30](https://github.com/mkdocstrings/python/commit/bfb5b303f4ea2187c15bccc688f7eba25e7edfcc) by Timothée Mazzucotelli). [Issue-219](https://github.com/mkdocstrings/python/issues/219) +- Sort objects without line numbers last instead of first ([681afb1](https://github.com/mkdocstrings/python/commit/681afb146225d98350a8eb2178aab07aec95fe6b) by Timothée Mazzucotelli). + ## [1.16.7](https://github.com/mkdocstrings/python/releases/tag/1.16.7) - 2025-03-20 [Compare with 1.16.6](https://github.com/mkdocstrings/python/compare/1.16.6...1.16.7)