From 84ba73e8ca4e54afd29a4fe67b8bfb022e76c794 Mon Sep 17 00:00:00 2001
From: Chris Wilson <14631+cdwilson@users.noreply.github.com>
Date: Mon, 17 Aug 2026 09:52:42 -0700
Subject: [PATCH 1/2] fix: Don't render environment paths in source block
labels
When an object's file lives in a virtual environment inside the current working directory (for example a pydantic `BaseModel` subclass documented with `preload_modules: [pydantic]` and an in-project `.venv`), source blocks were labeled with the environment path, like `.venv/lib/python3.10/site-packages/pydantic/main.py`.
The path is relative to the current working directory, so it slipped past the `is_absolute()` check that normally keeps environment paths out of rendered docs.
Add a `source_location` filter that strips everything up to and including a `site-packages` directory from displayed source paths, and use it for every source block label: merged `__init__`, class, and function, in both themes.
Centralizing the logic also fixes the label for single-file modules installed directly in `site-packages` (like `six.py`), whose package-relative path still contained the environment directory. Labels of objects belonging to the documented package itself are unchanged.
Issue-333: https://github.com/mkdocstrings/python/issues/333
PR-338: https://github.com/mkdocstrings/python/pull/338
---
src/mkdocstrings_handlers/python/__init__.py | 2 +
.../python/_internal/handler.py | 1 +
.../python/_internal/rendering.py | 25 +++++++
.../templates/material/_base/class.html.jinja | 16 +---
.../material/_base/function.html.jinja | 8 +-
.../readthedocs/_base/class.html.jinja | 16 +---
tests/test_handler.py | 75 +++++++++++++++++++
7 files changed, 108 insertions(+), 35 deletions(-)
diff --git a/src/mkdocstrings_handlers/python/__init__.py b/src/mkdocstrings_handlers/python/__init__.py
index dbad0355..c8927f7d 100644
--- a/src/mkdocstrings_handlers/python/__init__.py
+++ b/src/mkdocstrings_handlers/python/__init__.py
@@ -31,6 +31,7 @@
do_format_type_alias,
do_get_template,
do_order_members,
+ do_source_location,
do_split_path,
do_stash_crossref,
)
@@ -64,6 +65,7 @@
"do_format_type_alias",
"do_get_template",
"do_order_members",
+ "do_source_location",
"do_split_path",
"do_stash_crossref",
"get_handler",
diff --git a/src/mkdocstrings_handlers/python/_internal/handler.py b/src/mkdocstrings_handlers/python/_internal/handler.py
index 50e59b7e..cd1feb3b 100644
--- a/src/mkdocstrings_handlers/python/_internal/handler.py
+++ b/src/mkdocstrings_handlers/python/_internal/handler.py
@@ -345,6 +345,7 @@ def update_env(self, config: Any) -> None: # noqa: ARG002
self.env.filters["filter_objects"] = rendering.do_filter_objects
self.env.filters["stash_crossref"] = rendering.do_stash_crossref
self.env.filters["get_template"] = rendering.do_get_template
+ self.env.filters["source_location"] = rendering.do_source_location
self.env.filters["as_attributes_section"] = rendering.do_as_attributes_section
self.env.filters["as_functions_section"] = rendering.do_as_functions_section
self.env.filters["as_classes_section"] = rendering.do_as_classes_section
diff --git a/src/mkdocstrings_handlers/python/_internal/rendering.py b/src/mkdocstrings_handlers/python/_internal/rendering.py
index 446ad2d1..dcd4df61 100644
--- a/src/mkdocstrings_handlers/python/_internal/rendering.py
+++ b/src/mkdocstrings_handlers/python/_internal/rendering.py
@@ -11,6 +11,7 @@
from contextlib import suppress
from dataclasses import replace
from functools import lru_cache
+from pathlib import Path
from re import Pattern
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal, TypeVar
@@ -590,6 +591,30 @@ def do_get_template(obj: Object | Alias) -> str:
return f"{name}.html.jinja"
+def do_source_location(obj: Object | Alias) -> Path:
+ """Get the file path displayed in an object's source block label.
+
+ Environment paths are never displayed: when the object's file lives in
+ a `site-packages` directory (for example a virtual environment inside
+ the current working directory), the path below `site-packages` is
+ returned instead.
+
+ Parameters:
+ obj: A Griffe object.
+
+ Returns:
+ The file path to display.
+ """
+ relative_filepath = obj.relative_filepath
+ parts = relative_filepath.parts
+ if "site-packages" in parts:
+ anchor = len(parts) - 1 - parts[::-1].index("site-packages")
+ return Path(*parts[anchor + 1 :])
+ if relative_filepath.is_absolute():
+ return obj.relative_package_filepath
+ return relative_filepath
+
+
@pass_context
def do_as_attributes_section(
context: Context, # noqa: ARG001
diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/class.html.jinja b/src/mkdocstrings_handlers/python/templates/material/_base/class.html.jinja
index 57f9fd5a..17e9e29f 100644
--- a/src/mkdocstrings_handlers/python/templates/material/_base/class.html.jinja
+++ b/src/mkdocstrings_handlers/python/templates/material/_base/class.html.jinja
@@ -249,26 +249,14 @@ Context:
{% if "__init__" in all_members and all_members["__init__"].source %}
{% with init = all_members["__init__"] %}
- {{ lang.t("Source code in") }}
- {%- if init.relative_filepath.is_absolute() -%}
- {{ init.relative_package_filepath }}
- {%- else -%}
- {{ init.relative_filepath }}
- {%- endif -%}
-
+ {{ lang.t("Source code in") }} {{ init|source_location }}
{{ init.source|highlight(language="python", linestart=init.lineno or 0, linenums=True) }}
{% endwith %}
{% endif %}
{% elif class.source %}
- {{ lang.t("Source code in") }}
- {%- if class.relative_filepath.is_absolute() -%}
- {{ class.relative_package_filepath }}
- {%- else -%}
- {{ class.relative_filepath }}
- {%- endif -%}
-
+ {{ lang.t("Source code in") }} {{ class|source_location }}
{{ class.source|highlight(language="python", linestart=class.lineno or 0, linenums=True) }}
{% endif %}
diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/function.html.jinja b/src/mkdocstrings_handlers/python/templates/material/_base/function.html.jinja
index 3cfc7f30..c824226e 100644
--- a/src/mkdocstrings_handlers/python/templates/material/_base/function.html.jinja
+++ b/src/mkdocstrings_handlers/python/templates/material/_base/function.html.jinja
@@ -146,13 +146,7 @@ Context:
-#}
{% if config.show_source and function.source %}
- {{ lang.t("Source code in") }}
- {%- if function.relative_filepath.is_absolute() -%}
- {{ function.relative_package_filepath }}
- {%- else -%}
- {{ function.relative_filepath }}
- {%- endif -%}
-
+ {{ lang.t("Source code in") }} {{ function|source_location }}
{{ function.source|highlight(language="python", linestart=function.lineno or 0, linenums=True) }}
{% endif %}
diff --git a/src/mkdocstrings_handlers/python/templates/readthedocs/_base/class.html.jinja b/src/mkdocstrings_handlers/python/templates/readthedocs/_base/class.html.jinja
index 64b41ea6..5f011000 100644
--- a/src/mkdocstrings_handlers/python/templates/readthedocs/_base/class.html.jinja
+++ b/src/mkdocstrings_handlers/python/templates/readthedocs/_base/class.html.jinja
@@ -208,26 +208,14 @@ Context:
{% if "__init__" in class.all_members and class.all_members["__init__"].source %}
{% with init = class.all_members["__init__"] %}
- Source code in
- {%- if init.relative_filepath.is_absolute() -%}
- {{ init.relative_package_filepath }}
- {%- else -%}
- {{ init.relative_filepath }}
- {%- endif -%}
-
+ Source code in {{ init|source_location }}
{{ init.source|highlight(language="python", linestart=init.lineno or 0, linenums=True) }}
{% endwith %}
{% endif %}
{% elif class.source %}
- Source code in
- {%- if class.relative_filepath.is_absolute() -%}
- {{ class.relative_package_filepath }}
- {%- else -%}
- {{ class.relative_filepath }}
- {%- endif -%}
-
+ Source code in {{ class|source_location }}
{{ class.source|highlight(language="python", linestart=class.lineno or 0, linenums=True) }}
{% endif %}
diff --git a/tests/test_handler.py b/tests/test_handler.py
index 1cccd6c6..323d4266 100644
--- a/tests/test_handler.py
+++ b/tests/test_handler.py
@@ -11,6 +11,7 @@
from textwrap import dedent
from typing import TYPE_CHECKING
+import bs4
import mkdocstrings
import pytest
from griffe import (
@@ -333,3 +334,77 @@ def test_specifying_inventory_base_url(handler: PythonHandler) -> None:
# Assert the URL is based on the provided base URL
msg = "Expected inventory URL to start with base_url"
assert item_url.startswith(base_url), msg
+
+
+def _source_labels(html: str) -> list[Path]:
+ soup = bs4.BeautifulSoup(html, features="html.parser")
+ labels = []
+ for summary in soup.find_all("summary"):
+ if "Source code in" in summary.get_text():
+ code_tag = summary.find("code")
+ assert code_tag is not None
+ labels.append(Path(code_tag.get_text(strip=True)))
+ return labels
+
+
+def _write_site_packages_package(tmp_path: Path, *, single_module: bool) -> None:
+ """Lay out the issue-333 scenario: a package installed in a virtual environment inside the project.
+
+ The environment's `site-packages` directory is relative to the current
+ working directory, so the package's `relative_filepath` is relative too,
+ slipping past `is_absolute()` checks.
+ """
+ code = """
+ class Model:
+ '''Model docstring.'''
+
+ def __init__(self) -> None:
+ '''Init docstring.'''
+ self.model_attribute = 0
+
+ def method(self) -> None:
+ '''Method docstring.'''
+ """
+ site = tmp_path / "site-packages"
+ module_path = site / "pkg.py" if single_module else site / "pkg" / "__init__.py"
+ module_path.parent.mkdir(parents=True)
+ module_path.write_text(dedent(code), encoding="utf-8")
+
+
+@pytest.mark.parametrize(
+ "handler",
+ [
+ {"theme": "readthedocs"},
+ {"theme": {"name": "material"}},
+ ],
+ indirect=["handler"],
+)
+@pytest.mark.parametrize(
+ ("single_module", "extra_options", "expected_label"),
+ [
+ pytest.param(False, {"merge_init_into_class": True}, Path("pkg", "__init__.py"), id="merged-init"),
+ pytest.param(False, {}, Path("pkg", "__init__.py"), id="class-and-methods"),
+ pytest.param(True, {"merge_init_into_class": True}, Path("pkg.py"), id="single-module"),
+ ],
+)
+def test_no_environment_path_in_source_labels(
+ *,
+ tmp_path: Path,
+ monkeypatch: pytest.MonkeyPatch,
+ handler: PythonHandler,
+ single_module: bool,
+ extra_options: dict,
+ expected_label: Path,
+) -> None:
+ """Assert source labels never show an environment path."""
+ _write_site_packages_package(tmp_path, single_module=single_module)
+ monkeypatch.chdir(tmp_path)
+ # `collect()` reads the search paths lazily from this attribute.
+ handler._paths = [str(tmp_path / "site-packages")]
+ options = handler.get_options({"show_source": True, **extra_options})
+ html = handler.render(handler.collect("pkg.Model", options), options)
+ labels = _source_labels(html)
+ assert labels
+ assert set(labels) == {expected_label}
+ # The source bodies themselves are still rendered.
+ assert "model_attribute" in html
From 624974a419d6f0bf30acb4808675fdd66c6f302d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?=
Date: Mon, 17 Aug 2026 18:55:55 +0200
Subject: [PATCH 2/2] chore: Prepare release 2.0.7
---
CHANGELOG.md | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2ba4d956..f4d520b9 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).
+## [2.0.7](https://github.com/mkdocstrings/python/releases/tag/2.0.7) - 2026-08-17
+
+[Compare with 2.0.6](https://github.com/mkdocstrings/python/compare/2.0.6...2.0.7)
+
+### Bug Fixes
+
+- Don't render environment paths in source block labels ([84ba73e](https://github.com/mkdocstrings/python/commit/84ba73e8ca4e54afd29a4fe67b8bfb022e76c794) by Chris Wilson). [Issue-333](https://github.com/mkdocstrings/python/issues/333), [PR-338](https://github.com/mkdocstrings/python/pull/338)
+
## [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)