Skip to content

bug: merge_init_into_class renders inherited __init__ source (leaks site-packages paths) #333

Description

@cdwilson

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:

  1. The environment-specific .venv/... path leaks into published docs.
  2. 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-python v2.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 %}

Metadata

Metadata

Assignees

Labels

featureNew feature or request

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions