Description of the bug
With merge_init_into_class: true, the source block in
templates/material/_base/class.html.jinja unconditionally prefers
all_members["__init__"] over the class itself:
{% if config.merge_init_into_class %} {% if "__init__" in all_members and all_members["__init__"].source %}
all_members includes inherited members, so when a class does not
define __init__ itself (e.g. any pydantic BaseModel subclass, with
preload_modules: [pydantic] so griffe can resolve the base), the
rendered source block is pydantic.BaseModel.__init__ with a summary
like:
Source code in
.venv/lib/python3.10/site-packages/pydantic/main.py
instead of the documented class's own file. Two problems:
- The environment-specific
.venv/...path leaks into published docs. - The source shown is the base class's
__init__, which is not the
documented object's code at all.
Additionally, when the merged __init__ has no source, the block
renders nothing rather than falling back to the class source.
To Reproduce
Verified with mkdocs 1.6.1, mkdocstrings 1.0.4, mkdocstrings-python
2.0.5, griffe 2.1.0, pydantic 2.13.4 on Python 3.10.
mkdir -p repro/src/pkg repro/docs && cd repro python -m venv .venv && . .venv/bin/activate pip install mkdocs mkdocs-material "mkdocstrings[python]" pydantic
src/pkg/__init__.py:
from pydantic import BaseModel class Model(BaseModel): """A pydantic model without its own `__init__`.""" x: int = 0
docs/index.md:
::: pkg.Model
mkdocs.yml:
site_name: repro theme: name: material plugins: - mkdocstrings: handlers: python: paths: [src] options: merge_init_into_class: true preload_modules: [pydantic] show_source: true
Build and inspect the rendered source block:
$ mkdocs build $ grep -o "Source code in <code>[^<]*</code>" site/index.html Source code in <code>.venv/lib/python3.10/site-packages/pydantic/main.py</code>
Expected: src/pkg/__init__.py.
The mechanism, shown at the griffe level (run from the same directory):
import sysconfig import griffe loader = griffe.GriffeLoader( search_paths=["src", sysconfig.get_paths()["purelib"]] ) loader.load("pydantic") pkg = loader.load("pkg") loader.resolve_aliases() cls = pkg["Model"] init = cls.all_members["__init__"] print(cls.relative_filepath) # src/pkg/__init__.py print(init.inherited) # True print(init.relative_filepath) # .venv/lib/python3.10/site-packages/pydantic/main.py
Expected behavior
Only use __init__'s source when the class defines it itself
(griffe exposes inherited on the member); otherwise fall back to the
class's own source.
Environment information
python -m mkdocstrings_handlers.python._internal.debug # | xclip -selection clipboard- System: macOS-26.5.1-arm64-arm-64bit
- Python: cpython 3.10.20 (/.venv/bin/python3)
- Environment variables:
- Installed packages:
mkdocstrings-pythonv2.0.5
Additional context
I'm using a template override in
docs/templates/python/materialx/class.html.jinja, wired
up via the custom_templates option in mkdocs.yaml:
{% extends "_base/class.html.jinja" %} {% block source %} {% import "language.html.jinja" as lang with context %} {% if config.show_source %} {% if config.merge_init_into_class and "__init__" in all_members and not all_members["__init__"].inherited and all_members["__init__"].source %} {% with init = all_members["__init__"] %} <details class="mkdocstrings-source"> <summary>{{ lang.t("Source code in") }} <code> {%- if init.relative_filepath.is_absolute() -%} {{ init.relative_package_filepath }} {%- else -%} {{ init.relative_filepath }} {%- endif -%} </code></summary> {{ init.source|highlight(language="python", linestart=init.lineno or 0, linenums=True) }} </details> {% endwith %} {% elif class.source %} <details class="mkdocstrings-source"> <summary>{{ lang.t("Source code in") }} <code> {%- if class.relative_filepath.is_absolute() -%} {{ class.relative_package_filepath }} {%- else -%} {{ class.relative_filepath }} {%- endif -%} </code></summary> {{ class.source|highlight(language="python", linestart=class.lineno or 0, linenums=True) }} </details> {% endif %} {% endif %} {% endblock source %}