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)
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/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
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 8ab9b21f..8f544014 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
@@ -17,6 +18,8 @@
from griffe import (
Alias,
+ AliasResolutionError,
+ CyclicAliasError,
DocstringAttribute,
DocstringClass,
DocstringFunction,
@@ -43,26 +46,35 @@
_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"]
-"""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__,
}
@@ -246,7 +258,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.
@@ -266,7 +278,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.
@@ -396,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],
*,
@@ -455,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(