From 0fa2d2eb5d8023efed3f9dfe6446837a5b2228f7 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 8 Jul 2026 12:02:37 +0200
Subject: [PATCH 1/6] chore: Update sponsors section in README (#332)
Co-authored-by: github-actions[bot]
---
README.md | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 2d55958..fb895ae 100644
--- a/README.md
+++ b/README.md
@@ -108,7 +108,7 @@ dependencies = [
-
+
@@ -120,17 +120,15 @@ dependencies = [
-
-
-*And 7 more private sponsor(s).*
+*And 4 more private sponsor(s).*
From 6ff274911441ffaace47dd822fd92a3cdac84e65 Mon Sep 17 00:00:00 2001
From: Jonas Haag
Date: Fri, 7 Aug 2026 14:32:06 +0200
Subject: [PATCH 2/6] fix: Increase stash key length from 1 to 2
Cross-reference stash keys start with `_` and preserve the rendered identifier length. One-character and two-character identifiers therefore share a keyspace of only 62 keys. Once all keys are present, `_gen_stash_key` retries random choices forever. This happens for example in mkdocstrings when rendering an attribute value that has 62 names in it or more. We increase the minimum key length by 1, to reach a keyspace of 3844, making it very unlikely to hit the limit again.
PR-336: https://github.com/mkdocstrings/python/pull/336
---
src/mkdocstrings_handlers/python/_internal/rendering.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/mkdocstrings_handlers/python/_internal/rendering.py b/src/mkdocstrings_handlers/python/_internal/rendering.py
index d5ffc8e..e9efb53 100644
--- a/src/mkdocstrings_handlers/python/_internal/rendering.py
+++ b/src/mkdocstrings_handlers/python/_internal/rendering.py
@@ -107,7 +107,7 @@ class _StashCrossRefFilter:
@staticmethod
def _gen_key(length: int) -> str:
- return "_" + "".join(random.choice(string.ascii_letters + string.digits) for _ in range(max(1, length - 1))) # noqa: S311
+ return "_" + "".join(random.choice(string.ascii_letters + string.digits) for _ in range(max(2, length - 1))) # noqa: S311
def _gen_stash_key(self, length: int) -> str:
key = self._gen_key(length)
From 4b38b6ba6c3d1e09339cd5eb2c43145be84bda66 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?=
Date: Sun, 16 Aug 2026 15:08:46 +0200
Subject: [PATCH 3/6] fix: Don't pass unsupported options to Griffe parsers
Issue-337: https://github.com/mkdocstrings/python/issues/337
---
.../python/_internal/handler.py | 40 +++++++++++-
tests/test_config.py | 61 +++++++++++++++++++
2 files changed, 99 insertions(+), 2 deletions(-)
create mode 100644 tests/test_config.py
diff --git a/src/mkdocstrings_handlers/python/_internal/handler.py b/src/mkdocstrings_handlers/python/_internal/handler.py
index b84cd8b..0c26683 100644
--- a/src/mkdocstrings_handlers/python/_internal/handler.py
+++ b/src/mkdocstrings_handlers/python/_internal/handler.py
@@ -3,13 +3,14 @@
from __future__ import annotations
import glob
+import inspect
import os
import posixpath
import sys
from contextlib import suppress
from dataclasses import asdict
from pathlib import Path
-from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar
+from typing import TYPE_CHECKING, Any, BinaryIO, Callable, ClassVar
from griffe import (
AliasResolutionError,
@@ -18,6 +19,10 @@
ModulesCollection,
Parser,
load_extensions,
+ parse_auto,
+ parse_google,
+ parse_numpy,
+ parse_sphinx,
patch_loggers,
)
from mkdocs.exceptions import PluginError
@@ -54,6 +59,34 @@ def chdir(path: str) -> Iterator[None]:
patch_loggers(get_logger)
+_PARSER_FUNCTIONS: dict[Parser, Callable] = {
+ Parser.auto: parse_auto,
+ Parser.google: parse_google,
+ Parser.numpy: parse_numpy,
+ Parser.sphinx: parse_sphinx,
+}
+
+
+def _filter_parser_options(parser: Parser | None, options: dict[str, Any] | None) -> dict[str, Any] | None:
+ """Filter options unsupported by the selected Griffe parser."""
+ if parser is None or options is None:
+ return options
+
+ accepted_options = set(inspect.signature(_PARSER_FUNCTIONS[parser]).parameters) - {"docstring"}
+ filtered_options = {}
+ for name, value in options.items():
+ if name in accepted_options:
+ if parser is Parser.auto and name == "per_style_options":
+ filtered_options[name] = {
+ style: _filter_parser_options(Parser(style), style_options)
+ for style, style_options in value.items()
+ }
+ else:
+ filtered_options[name] = value
+ else:
+ _logger.warning(f"Ignoring unsupported {parser.value} docstring parser option: {name}")
+ return filtered_options
+
class PythonHandler(BaseHandler):
"""The Python handler class."""
@@ -195,7 +228,10 @@ def collect(self, identifier: str, options: PythonOptions) -> CollectorItem:
parser_name = options.docstring_style
parser = parser_name and Parser(parser_name)
- parser_options = options.docstring_options and asdict(options.docstring_options)
+ parser_options = options.docstring_options
+ if parser_options is not None:
+ parser_options = asdict(parser_options)
+ parser_options = _filter_parser_options(parser, parser_options)
if unknown_module:
extensions = self.normalize_extension_paths(options.extensions)
diff --git a/tests/test_config.py b/tests/test_config.py
new file mode 100644
index 0000000..5c12a41
--- /dev/null
+++ b/tests/test_config.py
@@ -0,0 +1,61 @@
+"""Tests for configuration options."""
+
+from __future__ import annotations
+
+import inspect
+from dataclasses import asdict, fields
+from typing import TYPE_CHECKING, Any
+
+import pytest
+from griffe import Parser, parse_google, parse_numpy, parse_sphinx
+
+from mkdocstrings_handlers.python import AutoStyleOptions, GoogleStyleOptions, NumpyStyleOptions, SphinxStyleOptions
+from mkdocstrings_handlers.python._internal.handler import _filter_parser_options
+
+if TYPE_CHECKING:
+ from collections.abc import Callable
+
+
+@pytest.mark.parametrize(
+ ("options_class", "parser"),
+ [
+ (GoogleStyleOptions, parse_google),
+ (NumpyStyleOptions, parse_numpy),
+ (SphinxStyleOptions, parse_sphinx),
+ ],
+)
+def test_style_options_match_griffe_parser(options_class: type[Any], parser: Callable[..., object]) -> None:
+ """Ensure style options stay in sync with Griffe parser options."""
+ option_names = {field.name for field in fields(options_class)}
+ parser_parameters = inspect.signature(parser).parameters
+ parser_option_names = set(parser_parameters) - {"docstring"}
+
+ assert parser_option_names <= option_names
+
+
+def test_filter_style_options(caplog: pytest.LogCaptureFixture) -> None:
+ """Ensure unsupported options are not passed to Griffe and are reported."""
+ options = asdict(SphinxStyleOptions())
+
+ filtered_options = _filter_parser_options(Parser.sphinx, options)
+
+ assert filtered_options == {
+ name: value for name, value in options.items() if name in inspect.signature(parse_sphinx).parameters
+ }
+ assert "warn_missing_types" in options
+ for name in set(options) - set(filtered_options or {}):
+ assert f"Ignoring unsupported sphinx docstring parser option: {name}" in caplog.text
+
+
+def test_filter_auto_style_options(caplog: pytest.LogCaptureFixture) -> None:
+ """Ensure unsupported options nested in auto style options are reported."""
+ options = asdict(AutoStyleOptions())
+
+ filtered_options = _filter_parser_options(Parser.auto, options)
+
+ assert filtered_options is not None
+ if "warn_missing_types" in inspect.signature(parse_sphinx).parameters:
+ assert "warn_missing_types" in filtered_options["per_style_options"]["sphinx"]
+ else:
+ assert "warn_missing_types" not in filtered_options["per_style_options"]["sphinx"]
+ assert "Ignoring unsupported sphinx docstring parser option: warn_missing_types" in caplog.text
From bb49b152f1ec712ba66fc8254143d19e34752a47 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?=
Date: Sun, 16 Aug 2026 15:45:35 +0200
Subject: [PATCH 4/6] tests: Re-add griffe as dev-dep to run API checks
---
pyproject.toml | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 96d3c71..dac83b5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -85,18 +85,19 @@ maintain = [
"yore>=0.3.3",
]
ci = [
+ "beautifulsoup4>=4.12.3",
"black>=25.1",
"duty>=1.6",
- "ruff>=0.4",
+ "griffe>=2.1",
+ "inline-snapshot>=0.25",
+ "mypy>=1.10",
"pytest>=8.2",
"pytest-cov>=5.0",
"pytest-randomly>=3.15",
"pytest-xdist>=3.6",
- "beautifulsoup4>=4.12.3",
- "inline-snapshot>=0.25",
- "mypy>=1.10",
"types-markdown>=3.6",
"types-pyyaml>=6.0",
+ "ruff>=0.4",
]
docs = [
"markdown-callouts>=0.4",
From 3c782f55846c56a2c8e533e73eae87cf05c9b5c7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?=
Date: Sun, 16 Aug 2026 15:51:04 +0200
Subject: [PATCH 5/6] ci: Fix type-checking issues
---
scripts/griffe_extensions.py | 9 +++++--
.../python/_internal/handler.py | 9 +++----
.../python/_internal/rendering.py | 25 ++++++++++---------
tests/test_api.py | 4 ++-
tests/test_rendering.py | 6 ++---
5 files changed, 30 insertions(+), 23 deletions(-)
diff --git a/scripts/griffe_extensions.py b/scripts/griffe_extensions.py
index 5a4447c..87106d4 100644
--- a/scripts/griffe_extensions.py
+++ b/scripts/griffe_extensions.py
@@ -23,9 +23,14 @@ def on_attribute_instance(
"""Fetch descriptions from `Field` annotations."""
if attr.docstring:
return
+ annotation = attr.annotation
+ if not isinstance(annotation, griffe.ExprSubscript) or not isinstance(annotation.slice, griffe.ExprTuple):
+ return
try:
- field: griffe.ExprCall = attr.annotation.slice.elements[1]
- except AttributeError:
+ field = annotation.slice.elements[1]
+ except IndexError:
+ return
+ if not isinstance(field, griffe.ExprCall):
return
if field.canonical_path == "mkdocstrings_handlers.python._internal.config._Field":
diff --git a/src/mkdocstrings_handlers/python/_internal/handler.py b/src/mkdocstrings_handlers/python/_internal/handler.py
index 0c26683..50e59b7 100644
--- a/src/mkdocstrings_handlers/python/_internal/handler.py
+++ b/src/mkdocstrings_handlers/python/_internal/handler.py
@@ -228,10 +228,9 @@ def collect(self, identifier: str, options: PythonOptions) -> CollectorItem:
parser_name = options.docstring_style
parser = parser_name and Parser(parser_name)
- parser_options = options.docstring_options
- if parser_options is not None:
- parser_options = asdict(parser_options)
- parser_options = _filter_parser_options(parser, parser_options)
+ parser_options: dict[str, Any] | None = None
+ if options.docstring_options is not None:
+ parser_options = _filter_parser_options(parser, asdict(options.docstring_options))
if unknown_module:
extensions = self.normalize_extension_paths(options.extensions)
@@ -239,7 +238,7 @@ def collect(self, identifier: str, options: PythonOptions) -> CollectorItem:
extensions=load_extensions(*extensions),
search_paths=self._paths,
docstring_parser=parser,
- docstring_options=parser_options,
+ docstring_options=parser_options, # type: ignore[arg-type]
modules_collection=self._modules_collection,
lines_collection=self._lines_collection,
allow_inspection=options.allow_inspection,
diff --git a/src/mkdocstrings_handlers/python/_internal/rendering.py b/src/mkdocstrings_handlers/python/_internal/rendering.py
index e9efb53..446ad2d 100644
--- a/src/mkdocstrings_handlers/python/_internal/rendering.py
+++ b/src/mkdocstrings_handlers/python/_internal/rendering.py
@@ -420,22 +420,20 @@ def _keep_object(name: str, filters: Sequence[tuple[Pattern, bool]]) -> bool:
def _parents(obj: Alias) -> set[str]:
- parent: Object | Alias = obj.parent
- parents = {obj.path, parent.path}
- if parent.is_alias:
- parents.add(parent.final_target.path)
- while parent.parent:
- parent = parent.parent
+ parents = {obj.path}
+ parent = obj.parent
+ while parent is not None:
parents.add(parent.path)
- if parent.is_alias:
+ if isinstance(parent, Alias):
parents.add(parent.final_target.path)
+ parent = parent.parent
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:
+ if isinstance(obj, Alias):
with suppress_errors:
if obj.final_target.path in _parents(obj):
continue
@@ -784,6 +782,8 @@ def expand_identifier(self, identifier: str) -> str:
obj = self.current_object
while identifier and identifier[0] == ".":
identifier = identifier[1:]
+ if obj.parent is None:
+ break
obj = obj.parent
identifier = f"{obj.path}.{identifier}" if identifier else obj.path
@@ -814,12 +814,13 @@ def get_context(self) -> AutorefsHookInterface.Context:
"module": "mod",
}.get(self.current_object.kind.value.lower(), "obj")
origin = self.current_object.path
- try:
- filepath = self.current_object.docstring.parent.filepath
- lineno = self.current_object.docstring.lineno or 0
- except AttributeError:
+ docstring = self.current_object.docstring
+ if docstring is None or docstring.parent is None:
filepath = self.current_object.filepath
lineno = 0
+ else:
+ filepath = docstring.parent.filepath
+ lineno = docstring.lineno or 0
return AutorefsHookInterface.Context(
domain="py",
diff --git a/tests/test_api.py b/tests/test_api.py
index 85432b5..a1c8412 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -36,7 +36,7 @@ def _fixture_public_api(loader: griffe.GriffeLoader) -> griffe.Module:
def _yield_public_objects(
- obj: griffe.Module | griffe.Class,
+ obj: griffe.Module | griffe.Class | griffe.Alias,
*,
modules: bool = False,
modulelevel: bool = True,
@@ -48,6 +48,7 @@ def _yield_public_objects(
if member.is_module:
if member.is_alias or not member.is_public:
continue
+ assert isinstance(member, griffe.Module)
if modules:
yield member
yield from _yield_public_objects(
@@ -62,6 +63,7 @@ def _yield_public_objects(
else:
continue
if member.is_class and not modulelevel:
+ assert isinstance(member, (griffe.Class, griffe.Alias))
yield from _yield_public_objects(
member,
modules=modules,
diff --git a/tests/test_rendering.py b/tests/test_rendering.py
index 91f945a..3709ec9 100644
--- a/tests/test_rendering.py
+++ b/tests/test_rendering.py
@@ -4,10 +4,10 @@
import re
from dataclasses import dataclass
-from typing import TYPE_CHECKING, Any, Callable
+from typing import TYPE_CHECKING, Any, Callable, cast
import pytest
-from griffe import ModulesCollection, temporary_visited_module
+from griffe import Alias, ModulesCollection, Object, temporary_visited_module
from mkdocstrings_handlers.python._internal import rendering
@@ -78,7 +78,7 @@ def test_filter_objects(names: list[str], filter_params: dict[str, Any], expecte
expected_names: Names expected to be kept.
"""
objects = {name: _FakeObject(name) for name in names}
- filtered = rendering.do_filter_objects(objects, **filter_params)
+ filtered = rendering.do_filter_objects(cast("dict[str, Object | Alias]", objects), **filter_params)
filtered_names = {obj.name for obj in filtered}
assert set(filtered_names) == set(expected_names)
From a025f4955e17546a6675f70bc5648904f0311ecb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?=
Date: Sun, 16 Aug 2026 15:51:48 +0200
Subject: [PATCH 6/6] chore: Prepare release 2.0.6
---
CHANGELOG.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ea1cfb..2ba4d95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,15 @@ 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).
+## [2.0.6](https://github.com/mkdocstrings/python/releases/tag/2.0.6) - 2026-08-16
+
+[Compare with 2.0.5](https://github.com/mkdocstrings/python/compare/2.0.5...2.0.6)
+
+### Bug Fixes
+
+- Don't pass unsupported options to Griffe parsers ([4b38b6b](https://github.com/mkdocstrings/python/commit/4b38b6ba6c3d1e09339cd5eb2c43145be84bda66) by Timothée Mazzucotelli). [Issue-337](https://github.com/mkdocstrings/python/issues/337)
+- Increase stash key length from 1 to 2 ([6ff2749](https://github.com/mkdocstrings/python/commit/6ff274911441ffaace47dd822fd92a3cdac84e65) by Jonas Haag). [PR-336](https://github.com/mkdocstrings/python/pull/336)
+
## [2.0.5](https://github.com/mkdocstrings/python/releases/tag/2.0.5) - 2026-06-19
[Compare with 2.0.4](https://github.com/mkdocstrings/python/compare/2.0.4...2.0.5)