diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2043c704..ec57bf03 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,14 @@ 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.15.0](https://github.com/mkdocstrings/python/releases/tag/1.15.0) - 2025-02-11
+
+[Compare with 1.14.6](https://github.com/mkdocstrings/python/compare/1.14.6...1.15.0)
+
+### Features
+
+- Support cross-referencing constructor parameters in instance attribute values ([f07bf58](https://github.com/mkdocstrings/python/commit/f07bf58a7358dea106032c7da27098e7617eefa0) by Timothée Mazzucotelli).
+
## [1.14.6](https://github.com/mkdocstrings/python/releases/tag/1.14.6) - 2025-02-07
[Compare with 1.14.5](https://github.com/mkdocstrings/python/compare/1.14.5...1.14.6)
diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py
index bfa74e5c..805055e0 100644
--- a/scripts/mkdocs_hooks.py
+++ b/scripts/mkdocs_hooks.py
@@ -1,7 +1,7 @@
"""Generate a JSON schema of the Python handler configuration."""
import json
-from dataclasses import fields
+from dataclasses import dataclass, fields
from os.path import join
from typing import Any
@@ -25,7 +25,12 @@ def on_post_build(config: MkDocsConfig, **kwargs: Any) -> None: # noqa: ARG001
if TypeAdapter is None:
logger.info("Pydantic is not installed, skipping JSON schema generation")
return
- adapter = TypeAdapter(PythonInputConfig)
+
+ @dataclass
+ class PythonHandlerSchema:
+ python: PythonInputConfig
+
+ adapter = TypeAdapter(PythonHandlerSchema)
schema = adapter.json_schema()
schema["$schema"] = "https://json-schema.org/draft-07/schema"
with open(join(config.site_dir, "schema.json"), "w") as file:
diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py
index e6bd3297..30cd2058 100644
--- a/src/mkdocstrings_handlers/python/handler.py
+++ b/src/mkdocstrings_handlers/python/handler.py
@@ -305,6 +305,11 @@ def update_env(self, config: Any) -> None: # noqa: ARG002
self.env.tests["existing_template"] = lambda template_name: template_name in self.env.list_templates()
def get_aliases(self, identifier: str) -> tuple[str, ...]: # noqa: D102 (ignore missing docstring)
+ if "(" in identifier:
+ identifier, parameter = identifier.split("(", 1)
+ parameter.removesuffix(")")
+ else:
+ parameter = ""
try:
data = self._modules_collection[identifier]
except (KeyError, AliasResolutionError):
@@ -315,7 +320,9 @@ def get_aliases(self, identifier: str) -> tuple[str, ...]: # noqa: D102 (ignore
if alias not in aliases:
aliases.append(alias)
except AliasResolutionError:
- return tuple(aliases)
+ pass
+ if parameter:
+ return tuple(f"{alias}({parameter})" for alias in aliases)
return tuple(aliases)
def normalize_extension_paths(self, extensions: Sequence) -> Sequence:
diff --git a/src/mkdocstrings_handlers/python/rendering.py b/src/mkdocstrings_handlers/python/rendering.py
index e298997a..b5c893c0 100644
--- a/src/mkdocstrings_handlers/python/rendering.py
+++ b/src/mkdocstrings_handlers/python/rendering.py
@@ -32,7 +32,7 @@
from mkdocstrings.loggers import get_logger
if TYPE_CHECKING:
- from collections.abc import Sequence
+ from collections.abc import Iterator, Sequence
from griffe import Attribute, Class, Function, Module
from jinja2 import Environment, Template
@@ -326,26 +326,47 @@ def repl(match: Match) -> str:
return Markup(text).format(**variables)
-def do_split_path(path: str, full_path: str) -> list[tuple[str, str]]:
+_split_path_re = re.compile(r"([.(]?)([\w]+)(\))?")
+_splitable_re = re.compile(r"[().]")
+
+
+def do_split_path(path: str, full_path: str) -> Iterator[tuple[str, str, str, str]]:
"""Split object paths for building cross-references.
Parameters:
path: The path to split.
+ full_path: The full path, used to compute correct paths for each part of the path.
- Returns:
- A list of pairs (title, full path).
+ Yields:
+ 4-tuples: prefix, word, full path, suffix.
"""
- if "." not in path:
- return [(path, full_path)]
- pairs = []
- full_path = ""
- for part in path.split("."):
- if full_path:
- full_path += f".{part}"
- else:
- full_path = part
- pairs.append((part, full_path))
- return pairs
+ # Path is a single word, yield full path directly.
+ if not _splitable_re.search(path):
+ yield ("", path, full_path, "")
+ return
+
+ current_path = ""
+ if path == full_path:
+ # Split full path and yield directly without storing data in a dict.
+ for match in _split_path_re.finditer(full_path):
+ prefix, word, suffix = match.groups()
+ current_path = f"{current_path}{prefix}{word}{suffix or ''}" if current_path else word
+ yield prefix or "", word, current_path, suffix or ""
+ return
+
+ # Split full path first to store tuples in a dict.
+ elements = {}
+ for match in _split_path_re.finditer(full_path):
+ prefix, word, suffix = match.groups()
+ current_path = f"{current_path}{prefix}{word}{suffix or ''}" if current_path else word
+ elements[word] = (prefix or "", word, current_path, suffix or "")
+
+ # Then split path and pick tuples from the dict.
+ first = True
+ for match in _split_path_re.finditer(path):
+ prefix, word, current_path, suffix = elements[match.group(2)]
+ yield "" if first else prefix, word, current_path, suffix
+ first = False
def _keep_object(name: str, filters: Sequence[tuple[Pattern, bool]]) -> bool:
diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/expression.html.jinja b/src/mkdocstrings_handlers/python/templates/material/_base/expression.html.jinja
index 5d7c07d5..781d46c7 100644
--- a/src/mkdocstrings_handlers/python/templates/material/_base/expression.html.jinja
+++ b/src/mkdocstrings_handlers/python/templates/material/_base/expression.html.jinja
@@ -31,7 +31,8 @@ which is a tree-like structure representing a Python expression.
{%- elif annotation_path == "full" -%}
{%- set annotation = full -%}
{%- endif -%}
- {%- for title, path in annotation|split_path(full) -%}
+ {%- for prefix, title, path, suffix in annotation|split_path(full) -%}
+ {{ prefix }}
{%- if not signature -%}
{#- Always render cross-references outside of signatures. We don't need to stash them. -#}
{{ title }}
@@ -44,7 +45,7 @@ which is a tree-like structure representing a Python expression.
{#- We're in a signature but cross-references are disabled, we just render the title. -#}
{{ title }}
{%- endif -%}
- {%- if not loop.last -%}.{%- endif -%}
+ {{ suffix }}
{%- endfor -%}
{%- endwith -%}
{%- endmacro -%}