diff --git a/.copier-answers.yml b/.copier-answers.yml
index 90ce2e79..1dc4ac4d 100644
--- a/.copier-answers.yml
+++ b/.copier-answers.yml
@@ -1,5 +1,5 @@
# Changes here will be overwritten by Copier
-_commit: 1.1.4
+_commit: 1.2.0
_src_path: gh:mkdocstrings/handler-template
author_email: dev@pawamoy.fr
author_fullname: Timothée Mazzucotelli
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e422aeb8..6940069d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,13 +29,16 @@ jobs:
- name: Fetch all tags
run: git fetch --depth=1 --tags
- - name: Set up Python
+ - name: Setup Python
uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.12"
- - name: Install uv
- run: pip install uv
+ - name: Setup uv
+ uses: astral-sh/setup-uv@v3
+ with:
+ enable-cache: true
+ cache-dependency-glob: pyproject.toml
- name: Install dependencies
run: make setup
@@ -63,11 +66,11 @@ jobs:
echo 'jobs=[
{"os": "macos-latest"},
{"os": "windows-latest"},
- {"python-version": "3.9"},
{"python-version": "3.10"},
{"python-version": "3.11"},
{"python-version": "3.12"},
- {"python-version": "3.13"}
+ {"python-version": "3.13"},
+ {"python-version": "3.14"}
]' | tr -d '[:space:]' >> $GITHUB_OUTPUT
else
echo 'jobs=[
@@ -87,31 +90,35 @@ jobs:
- macos-latest
- windows-latest
python-version:
- - "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- "3.13"
+ - "3.14"
resolution:
- highest
- lowest-direct
exclude: ${{ fromJSON(needs.exclude-test-jobs.outputs.jobs) }}
runs-on: ${{ matrix.os }}
- continue-on-error: ${{ matrix.python-version == '3.13' }}
+ continue-on-error: ${{ matrix.python-version == '3.14' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- - name: Set up Python
+ - name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- - name: Install uv
- run: pip install uv
+ - name: Setup uv
+ uses: astral-sh/setup-uv@v3
+ with:
+ enable-cache: true
+ cache-dependency-glob: pyproject.toml
+ cache-suffix: py${{ matrix.python-version }}
- name: Install dependencies
env:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d82736f7..45bcf5a4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,32 +14,30 @@ jobs:
- name: Fetch all tags
run: git fetch --depth=1 --tags
- name: Setup Python
- uses: actions/setup-python@v4
- - name: Install build
- if: github.repository_owner == 'pawamoy-insiders'
- run: python -m pip install build
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+ - name: Setup uv
+ uses: astral-sh/setup-uv@v3
- name: Build dists
if: github.repository_owner == 'pawamoy-insiders'
- run: python -m build
+ run: uv tool run --from build pyproject-build
- name: Upload dists artifact
uses: actions/upload-artifact@v4
if: github.repository_owner == 'pawamoy-insiders'
with:
name: python-insiders
path: ./dist/*
- - name: Install git-changelog
- if: github.repository_owner != 'pawamoy-insiders'
- run: pip install git-changelog
- name: Prepare release notes
if: github.repository_owner != 'pawamoy-insiders'
- run: git-changelog --release-notes > release-notes.md
+ run: uv tool run git-changelog --release-notes > release-notes.md
- name: Create release with assets
- uses: softprops/action-gh-release@v1
+ uses: softprops/action-gh-release@v2
if: github.repository_owner == 'pawamoy-insiders'
with:
files: ./dist/*
- name: Create release
- uses: softprops/action-gh-release@v1
+ uses: softprops/action-gh-release@v2
if: github.repository_owner != 'pawamoy-insiders'
with:
body_path: release-notes.md
diff --git a/.gitignore b/.gitignore
index 41fee62d..9fea0472 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@
/.pdm-build/
/htmlcov/
/site/
+uv.lock
# cache
.cache/
diff --git a/.gitpod.dockerfile b/.gitpod.dockerfile
deleted file mode 100644
index 1590b415..00000000
--- a/.gitpod.dockerfile
+++ /dev/null
@@ -1,6 +0,0 @@
-FROM gitpod/workspace-full
-USER gitpod
-ENV PIP_USER=no
-RUN pip3 install pipx; \
- pipx install uv; \
- pipx ensurepath
diff --git a/.gitpod.yml b/.gitpod.yml
deleted file mode 100644
index 23a3c2b7..00000000
--- a/.gitpod.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-vscode:
- extensions:
- - ms-python.python
-
-image:
- file: .gitpod.dockerfile
-
-ports:
-- port: 8000
- onOpen: notify
-
-tasks:
-- init: make setup
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f1668147..9d9f4c0d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,32 @@ 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.12.1](https://github.com/mkdocstrings/python/releases/tag/1.12.1) - 2024-10-14
+
+[Compare with 1.12.0](https://github.com/mkdocstrings/python/compare/1.12.0...1.12.1)
+
+### Bug Fixes
+
+- Don't escape parameter default values ([9dee4d4](https://github.com/mkdocstrings/python/commit/9dee4d4f8e1258e99c19dc7b2b18d3e9090de79b) by Timothée Mazzucotelli). [Issue-191](https://github.com/mkdocstrings/python/issues/191)
+
+## [1.12.0](https://github.com/mkdocstrings/python/releases/tag/1.12.0) - 2024-10-12
+
+[Compare with 1.11.1](https://github.com/mkdocstrings/python/compare/1.11.1...1.12.0)
+
+### Build
+
+- Drop support for Python 3.8 ([6615c91](https://github.com/mkdocstrings/python/commit/6615c91cdc035bc0c2fdd12f3952ff84f5e1c04e) by Timothée Mazzucotelli).
+
+### Features
+
+- Auto-summary of members ([7f9757d](https://github.com/mkdocstrings/python/commit/7f9757d1584555edebc56f1aefe6cc8242e6c8bb) by Timothée Mazzucotelli).
+- Render function overloads ([0f2c25c](https://github.com/mkdocstrings/python/commit/0f2c25c9ed7f6c5c93ff13df214f02edfd3a4cb1) by Timothée Mazzucotelli).
+- Parameter headings, more automatic cross-references ([0176b83](https://github.com/mkdocstrings/python/commit/0176b83f21ae02d345489c93cca3baf51f8bc58c) by Timothée Mazzucotelli).
+
+### Code Refactoring
+
+- Declare default CSS symbol colors under :host as well ([3b9dba2](https://github.com/mkdocstrings/python/commit/3b9dba2709a8668e379c6ce1536cb1714971b3f4) by James McDonnell). [PR-186](https://github.com/mkdocstrings/python/pull/186)
+
## [1.11.1](https://github.com/mkdocstrings/python/releases/tag/1.11.1) - 2024-09-03
[Compare with 1.11.0](https://github.com/mkdocstrings/python/compare/1.11.0...1.11.1)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bbc08404..3e3dc294 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -23,12 +23,11 @@ make setup
> You can install it with:
>
> ```bash
-> python3 -m pip install --user pipx
-> pipx install uv
+> curl -LsSf https://astral.sh/uv/install.sh | sh
> ```
>
> Now you can try running `make setup` again,
-> or simply `uv install`.
+> or simply `uv sync`.
You now have the dependencies installed.
diff --git a/config/ruff.toml b/config/ruff.toml
index e3c9ec30..4c91b364 100644
--- a/config/ruff.toml
+++ b/config/ruff.toml
@@ -1,4 +1,4 @@
-target-version = "py38"
+target-version = "py39"
line-length = 120
[lint]
diff --git a/devdeps.txt b/devdeps.txt
deleted file mode 100644
index e0afd7e2..00000000
--- a/devdeps.txt
+++ /dev/null
@@ -1,32 +0,0 @@
-# dev
-editables>=0.5
-
-# maintenance
-build>=1.2
-git-changelog>=2.5
-twine>=5.0; python_version < '3.13'
-
-# ci
-duty>=1.4
-ruff>=0.4
-pytest>=8.2
-pytest-cov>=5.0
-pytest-randomly>=3.15
-pytest-xdist>=3.6
-mypy>=1.10
-types-markdown>=3.6
-types-pyyaml>=6.0
-
-# docs
-black>=24.4
-markdown-callouts>=0.4
-markdown-exec>=1.8
-mkdocs>=1.6
-mkdocs-coverage>=1.0
-mkdocs-gen-files>=0.5
-mkdocs-git-committers-plugin-2>=2.3
-mkdocs-literate-nav>=0.6
-mkdocs-material>=9.5
-mkdocs-minify-plugin>=0.8
-mkdocstrings[python]>=0.25
-tomli>=2.0; python_version < '3.11'
diff --git a/docs/usage/configuration/headings.md b/docs/usage/configuration/headings.md
index 63950206..467779e4 100644
--- a/docs/usage/configuration/headings.md
+++ b/docs/usage/configuration/headings.md
@@ -59,7 +59,6 @@ plugins:
## `parameter_headings`
-[:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } —
[:octicons-tag-24: Insiders 1.6.0](../../insiders/changelog.md#1.6.0)
- **:octicons-package-24: Type [`bool`][] :material-equal: `False`{ title="default value" }**
diff --git a/docs/usage/configuration/members.md b/docs/usage/configuration/members.md
index 119d8294..220a26fe 100644
--- a/docs/usage/configuration/members.md
+++ b/docs/usage/configuration/members.md
@@ -552,7 +552,6 @@ package
## `summary`
-[:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } —
[:octicons-tag-24: Insiders 1.2.0](../../insiders/changelog.md#1.2.0)
- **:octicons-package-24: Type
:material-equal: `False`{ title="default value" }**
diff --git a/duties.py b/duties.py
index f1909cc1..3864e74e 100644
--- a/duties.py
+++ b/duties.py
@@ -7,11 +7,13 @@
from contextlib import contextmanager
from importlib.metadata import version as pkgversion
from pathlib import Path
-from typing import TYPE_CHECKING, Iterator
+from typing import TYPE_CHECKING
from duty import duty, tools
if TYPE_CHECKING:
+ from collections.abc import Iterator
+
from duty.context import Context
@@ -53,7 +55,7 @@ def changelog(ctx: Context, bump: str = "") -> None:
ctx.run(tools.git_changelog(bump=bump or None), title="Updating changelog")
-@duty(pre=["check_quality", "check_types", "check_docs", "check-api"])
+@duty(pre=["check-quality", "check-types", "check-docs", "check-api"])
def check(ctx: Context) -> None:
"""Check it all!"""
@@ -116,23 +118,33 @@ def docs(ctx: Context, *cli_args: str, host: str = "127.0.0.1", port: int = 8000
@duty
-def docs_deploy(ctx: Context) -> None:
- """Deploy the documentation to GitHub pages."""
+def docs_deploy(ctx: Context, *, force: bool = False) -> None:
+ """Deploy the documentation to GitHub pages.
+
+ Parameters:
+ force: Whether to force deployment, even from non-Insiders version.
+ """
os.environ["DEPLOY"] = "true"
with material_insiders() as insiders:
if not insiders:
ctx.run(lambda: False, title="Not deploying docs without Material for MkDocs Insiders!")
- origin = ctx.run("git config --get remote.origin.url", silent=True)
+ origin = ctx.run("git config --get remote.origin.url", silent=True, allow_overrides=False)
if "pawamoy-insiders/mkdocstrings-python" in origin:
ctx.run(
"git remote add upstream git@github.com:mkdocstrings/python",
silent=True,
nofail=True,
+ allow_overrides=False,
)
ctx.run(
tools.mkdocs.gh_deploy(remote_name="upstream", force=True),
title="Deploying documentation",
)
+ elif force:
+ ctx.run(
+ tools.mkdocs.gh_deploy(force=True),
+ title="Deploying documentation",
+ )
else:
ctx.run(
lambda: False,
diff --git a/mkdocs.yml b/mkdocs.yml
index 19aa90d9..2d546126 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -180,9 +180,10 @@ plugins:
signature_crossrefs: true
summary: true
unwrap_annotated: true
-- git-committers:
+- git-revision-date-localized:
enabled: !ENV [DEPLOY, false]
- repository: mkdocstrings/python
+ enable_creation_date: true
+ type: timeago
- minify:
minify_html: !ENV [DEPLOY, false]
- group:
diff --git a/pyproject.toml b/pyproject.toml
index 0eccf7fe..636a67fe 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,7 +8,7 @@ description = "A Python handler for mkdocstrings."
authors = [{name = "Timothée Mazzucotelli", email = "dev@pawamoy.fr"}]
license = {text = "ISC"}
readme = "README.md"
-requires-python = ">=3.8"
+requires-python = ">=3.9"
keywords = []
dynamic = ["version"]
classifiers = [
@@ -17,14 +17,15 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Topic :: Documentation",
"Topic :: Software Development",
+ "Topic :: Software Development :: Documentation",
"Topic :: Utilities",
"Typing :: Typed",
]
@@ -58,7 +59,6 @@ source-includes = [
"scripts",
"share",
"tests",
- "devdeps.txt",
"duties.py",
"mkdocs.yml",
"*.md",
@@ -69,3 +69,39 @@ source-includes = [
data = [
{path = "share/**/*", relative-to = "."},
]
+
+[tool.uv]
+dev-dependencies = [
+ # dev
+ "editables>=0.5",
+
+ # maintenance
+ "build>=1.2",
+ "git-changelog>=2.5",
+ "twine>=5.1",
+
+ # ci
+ "duty>=1.4",
+ "ruff>=0.4",
+ "pytest>=8.2",
+ "pytest-cov>=5.0",
+ "pytest-randomly>=3.15",
+ "pytest-xdist>=3.6",
+ "mypy>=1.10",
+ "types-markdown>=3.6",
+ "types-pyyaml>=6.0",
+
+ # docs
+ "black>=24.4",
+ "markdown-callouts>=0.4",
+ "markdown-exec>=1.8",
+ "mkdocs>=1.6",
+ "mkdocs-coverage>=1.0",
+ "mkdocs-gen-files>=0.5",
+ "mkdocs-git-revision-date-localized-plugin>=1.2",
+ "mkdocs-literate-nav>=0.6",
+ "mkdocs-material>=9.5",
+ "mkdocs-minify-plugin>=0.8",
+ # YORE: EOL 3.10: Remove line.
+ "tomli>=2.0; python_version < '3.11'",
+]
\ No newline at end of file
diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py
index b2f6d3e4..51ebe2f3 100644
--- a/scripts/gen_credits.py
+++ b/scripts/gen_credits.py
@@ -5,17 +5,18 @@
import os
import sys
from collections import defaultdict
+from collections.abc import Iterable
from importlib.metadata import distributions
from itertools import chain
from pathlib import Path
from textwrap import dedent
-from typing import Dict, Iterable, Union
+from typing import Union
from jinja2 import StrictUndefined
from jinja2.sandbox import SandboxedEnvironment
from packaging.requirements import Requirement
-# TODO: Remove once support for Python 3.10 is dropped.
+# YORE: EOL 3.10: Replace block with line 2.
if sys.version_info >= (3, 11):
import tomllib
else:
@@ -26,11 +27,10 @@
pyproject = tomllib.load(pyproject_file)
project = pyproject["project"]
project_name = project["name"]
-with project_dir.joinpath("devdeps.txt").open() as devdeps_file:
- devdeps = [line.strip() for line in devdeps_file if line.strip() and not line.strip().startswith(("-e", "#"))]
+devdeps = [dep for dep in pyproject["tool"]["uv"]["dev-dependencies"] if not dep.startswith("-e")]
-PackageMetadata = Dict[str, Union[str, Iterable[str]]]
-Metadata = Dict[str, PackageMetadata]
+PackageMetadata = dict[str, Union[str, Iterable[str]]]
+Metadata = dict[str, PackageMetadata]
def _merge_fields(metadata: dict) -> PackageMetadata:
diff --git a/scripts/insiders.py b/scripts/insiders.py
index 15212486..849c6314 100644
--- a/scripts/insiders.py
+++ b/scripts/insiders.py
@@ -10,13 +10,16 @@
from datetime import date, datetime, timedelta
from itertools import chain
from pathlib import Path
-from typing import Iterable, cast
+from typing import TYPE_CHECKING, cast
from urllib.error import HTTPError
from urllib.parse import urljoin
from urllib.request import urlopen
import yaml
+if TYPE_CHECKING:
+ from collections.abc import Iterable
+
logger = logging.getLogger(f"mkdocs.logs.{__name__}")
diff --git a/scripts/make b/scripts/make
index d898022e..ac430624 100755
--- a/scripts/make
+++ b/scripts/make
@@ -9,12 +9,10 @@ import subprocess
import sys
from contextlib import contextmanager
from pathlib import Path
+from textwrap import dedent
from typing import Any, Iterator
-PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.8 3.9 3.10 3.11 3.12 3.13").split()
-
-exe = ""
-prefix = ""
+PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.9 3.10 3.11 3.12 3.13 3.14").split()
def shell(cmd: str, capture_output: bool = False, **kwargs: Any) -> str | None:
@@ -37,17 +35,13 @@ def environ(**kwargs: str) -> Iterator[None]:
os.environ.update(original)
-def uv_install() -> None:
+def uv_install(venv: Path) -> None:
"""Install dependencies using uv."""
- uv_opts = ""
- if "UV_RESOLUTION" in os.environ:
- uv_opts = f"--resolution={os.getenv('UV_RESOLUTION')}"
- requirements = shell(f"uv pip compile {uv_opts} pyproject.toml devdeps.txt", capture_output=True)
- shell("uv pip install -r -", input=requirements, text=True)
- if "CI" not in os.environ:
- shell("uv pip install --no-deps -e .")
- else:
- shell("uv pip install --no-deps .")
+ with environ(UV_PROJECT_ENVIRONMENT=str(venv), PYO3_USE_ABI3_FORWARD_COMPATIBILITY="1"):
+ if "CI" in os.environ:
+ shell("uv sync --no-editable")
+ else:
+ shell("uv sync")
def setup() -> None:
@@ -59,7 +53,7 @@ def setup() -> None:
default_venv = Path(".venv")
if not default_venv.exists():
shell("uv venv --python python")
- uv_install()
+ uv_install(default_venv)
if PYTHON_VERSIONS:
for version in PYTHON_VERSIONS:
@@ -67,39 +61,22 @@ def setup() -> None:
venv_path = Path(f".venvs/{version}")
if not venv_path.exists():
shell(f"uv venv --python {version} {venv_path}")
- with environ(VIRTUAL_ENV=str(venv_path.resolve())):
- uv_install()
-
-
-def activate(path: str) -> None:
- """Activate a virtual environment."""
- global exe, prefix # noqa: PLW0603
-
- if (bin := Path(path, "bin")).exists():
- activate_script = bin / "activate_this.py"
- elif (scripts := Path(path, "Scripts")).exists():
- activate_script = scripts / "activate_this.py"
- exe = ".exe"
- prefix = f"{path}/Scripts/"
- else:
- raise ValueError(f"make: activate: Cannot find activation script in {path}")
-
- if not activate_script.exists():
- raise ValueError(f"make: activate: Cannot find activation script in {path}")
-
- exec(activate_script.read_text(), {"__file__": str(activate_script)}) # noqa: S102
+ with environ(UV_PROJECT_ENVIRONMENT=str(venv_path.resolve())):
+ uv_install(venv_path)
-def run(version: str, cmd: str, *args: str, **kwargs: Any) -> None:
+def run(version: str, cmd: str, *args: str, no_sync: bool = False, **kwargs: Any) -> None:
"""Run a command in a virtual environment."""
kwargs = {"check": True, **kwargs}
+ uv_run = ["uv", "run"]
+ if no_sync:
+ uv_run.append("--no-sync")
if version == "default":
- activate(".venv")
- subprocess.run([f"{prefix}{cmd}{exe}", *args], **kwargs) # noqa: S603, PLW1510
+ with environ(UV_PROJECT_ENVIRONMENT=".venv"):
+ subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510
else:
- activate(f".venvs/{version}")
- os.environ["MULTIRUN"] = "1"
- subprocess.run([f"{prefix}{cmd}{exe}", *args], **kwargs) # noqa: S603, PLW1510
+ with environ(UV_PROJECT_ENVIRONMENT=f".venvs/{version}", MULTIRUN="1"):
+ subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510
def multirun(cmd: str, *args: str, **kwargs: Any) -> None:
@@ -124,10 +101,10 @@ def clean() -> None:
for path in paths_to_clean:
shell(f"rm -rf {path}")
- cache_dirs = [".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__"]
- for dirpath in Path(".").rglob("*"):
- if any(dirpath.match(pattern) for pattern in cache_dirs) and not (dirpath.match(".venv") or dirpath.match(".venvs")):
- shutil.rmtree(path, ignore_errors=True)
+ cache_dirs = {".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__"}
+ for dirpath in Path(".").rglob("*/"):
+ if dirpath.parts[0] not in (".venv", ".venvs") and dirpath.name in cache_dirs:
+ shutil.rmtree(dirpath, ignore_errors=True)
def vscode() -> None:
@@ -143,22 +120,25 @@ def main() -> int:
if len(args) > 1:
run("default", "duty", "--help", args[1])
else:
- print("Available commands") # noqa: T201
- print(" help Print this help. Add task name to print help.") # noqa: T201
- print(" setup Setup all virtual environments (install dependencies).") # noqa: T201
- print(" run Run a command in the default virtual environment.") # noqa: T201
- print(" multirun Run a command for all configured Python versions.") # noqa: T201
- print(" allrun Run a command in all virtual environments.") # noqa: T201
- print(" 3.x Run a command in the virtual environment for Python 3.x.") # noqa: T201
- print(" clean Delete build artifacts and cache files.") # noqa: T201
- print(" vscode Configure VSCode to work on this project.") # noqa: T201
- try:
- run("default", "python", "-V", capture_output=True)
- except (subprocess.CalledProcessError, ValueError):
- pass
- else:
- print("\nAvailable tasks") # noqa: T201
- run("default", "duty", "--list")
+ print(
+ dedent(
+ """
+ Available commands
+ help Print this help. Add task name to print help.
+ setup Setup all virtual environments (install dependencies).
+ run Run a command in the default virtual environment.
+ multirun Run a command for all configured Python versions.
+ allrun Run a command in all virtual environments.
+ 3.x Run a command in the virtual environment for Python 3.x.
+ clean Delete build artifacts and cache files.
+ vscode Configure VSCode to work on this project.
+ """
+ ),
+ flush=True,
+ ) # noqa: T201
+ if os.path.exists(".venv"):
+ print("\nAvailable tasks", flush=True) # noqa: T201
+ run("default", "duty", "--list", no_sync=True)
return 0
while args:
diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py
index ef93ee3b..0aac3cdc 100644
--- a/src/mkdocstrings_handlers/python/handler.py
+++ b/src/mkdocstrings_handlers/python/handler.py
@@ -10,7 +10,7 @@
from collections import ChainMap
from contextlib import suppress
from pathlib import Path
-from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar, Iterator, Mapping, Sequence
+from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar
from griffe import (
AliasResolutionError,
@@ -29,6 +29,8 @@
from mkdocstrings_handlers.python import rendering
if TYPE_CHECKING:
+ from collections.abc import Iterator, Mapping, Sequence
+
from markdown import Markdown
@@ -102,6 +104,7 @@ class PythonHandler(BaseHandler):
"show_docstring_yields": True,
"show_source": True,
"show_bases": True,
+ "show_inheritance_diagram": False,
"show_submodules": False,
"group_by_category": True,
"heading_level": 2,
@@ -116,6 +119,8 @@ class PythonHandler(BaseHandler):
"summary": False,
"show_labels": True,
"unwrap_annotated": False,
+ "parameter_headings": False,
+ "modernize_annotations": False,
}
"""Default handler configuration.
@@ -123,6 +128,7 @@ class PythonHandler(BaseHandler):
find_stubs_package (bool): Whether to load stubs package (package-stubs) when extracting docstrings. Default `False`.
allow_inspection (bool): Whether to allow inspecting modules when visiting them is not possible. Default: `True`.
show_bases (bool): Show the base classes of a class. Default: `True`.
+ show_inheritance_diagram (bool): Show the inheritance diagram of a class using Mermaid. Default: `False`.
show_source (bool): Show the source code of this object. Default: `True`.
preload_modules (list[str] | None): Pre-load modules that are
not specified directly in autodoc instructions (`::: identifier`).
@@ -136,6 +142,7 @@ class PythonHandler(BaseHandler):
Attributes: Headings options:
heading_level (int): The initial heading level to use. Default: `2`.
+ parameter_headings (bool): Whether to render headings for parameters (therefore showing parameters in the ToC). Default: `False`.
show_root_heading (bool): Show the heading of the object at the root of the documentation tree
(i.e. the object referenced by the identifier after `:::`). Default: `False`.
show_root_toc_entry (bool): If the root heading is not shown, at least add a ToC entry for it. Default: `True`.
@@ -196,6 +203,7 @@ class PythonHandler(BaseHandler):
separate_signature (bool): Whether to put the whole signature in a code block below the heading.
If Black is installed, the signature is also formatted using it. Default: `False`.
unwrap_annotated (bool): Whether to unwrap `Annotated` types to show only the type without the annotations. Default: `False`.
+ modernize_annotations (bool): Whether to modernize annotations, for example `Optional[str]` into `str | None`. Default: `False`.
"""
def __init__(
@@ -270,7 +278,7 @@ def load_inventory(
) -> Iterator[tuple[str, str]]:
"""Yield items and their URLs from an inventory file streamed from `in_file`.
- This implements mkdocstrings' `load_inventory` "protocol" (see [`mkdocstrings.plugin`][mkdocstrings.plugin]).
+ This implements mkdocstrings' `load_inventory` "protocol" (see [`mkdocstrings.plugin`][]).
Arguments:
in_file: The binary file-like object to read the inventory from.
@@ -424,7 +432,7 @@ def update_env(self, md: Markdown, config: dict) -> None:
self.env.filters["format_signature"] = rendering.do_format_signature
self.env.filters["format_attribute"] = rendering.do_format_attribute
self.env.filters["filter_objects"] = rendering.do_filter_objects
- self.env.filters["stash_crossref"] = lambda ref, length: ref
+ self.env.filters["stash_crossref"] = rendering.do_stash_crossref
self.env.filters["get_template"] = rendering.do_get_template
self.env.filters["as_attributes_section"] = rendering.do_as_attributes_section
self.env.filters["as_functions_section"] = rendering.do_as_functions_section
diff --git a/src/mkdocstrings_handlers/python/rendering.py b/src/mkdocstrings_handlers/python/rendering.py
index 2c4a4893..a7ea38f7 100644
--- a/src/mkdocstrings_handlers/python/rendering.py
+++ b/src/mkdocstrings_handlers/python/rendering.py
@@ -8,12 +8,17 @@
import string
import sys
import warnings
-from functools import lru_cache, partial
+from functools import lru_cache
from pathlib import Path
-from typing import TYPE_CHECKING, Any, Callable, Match, Pattern, Sequence
+from re import Match, Pattern
+from typing import TYPE_CHECKING, Any, Callable, ClassVar
from griffe import (
Alias,
+ DocstringAttribute,
+ DocstringClass,
+ DocstringFunction,
+ DocstringModule,
DocstringSectionAttributes,
DocstringSectionClasses,
DocstringSectionFunctions,
@@ -26,6 +31,8 @@
from mkdocstrings.loggers import get_logger
if TYPE_CHECKING:
+ from collections.abc import Sequence
+
from griffe import Attribute, Class, Function, Module
from jinja2 import Environment, Template
from jinja2.runtime import Context
@@ -80,24 +87,26 @@ def do_format_code(code: str, line_length: int) -> str:
return formatter(code, line_length)
-_stash_key_alphabet = string.ascii_letters + string.digits
-
+class _StashCrossRefFilter:
+ stash: ClassVar[dict[str, str]] = {}
-def _gen_key(length: int) -> str:
- return "_" + "".join(random.choice(_stash_key_alphabet) for _ in range(max(1, length - 1))) # noqa: S311
+ @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
+ def _gen_stash_key(self, length: int) -> str:
+ key = self._gen_key(length)
+ while key in self.stash:
+ key = self._gen_key(length)
+ return key
-def _gen_stash_key(stash: dict[str, str], length: int) -> str:
- key = _gen_key(length)
- while key in stash:
- key = _gen_key(length)
- return key
+ def __call__(self, crossref: str, *, length: int) -> str:
+ key = self._gen_stash_key(length)
+ self.stash[key] = crossref
+ return key
-def _stash_crossref(stash: dict[str, str], crossref: str, *, length: int) -> str:
- key = _gen_stash_key(stash, length)
- stash[key] = crossref
- return key
+do_stash_crossref = _StashCrossRefFilter()
def _format_signature(name: Markup, signature: str, line_length: int) -> str:
@@ -126,7 +135,7 @@ def do_format_signature(
line_length: int,
*,
annotations: bool | None = None,
- crossrefs: bool = False,
+ crossrefs: bool = False, # noqa: ARG001
) -> str:
"""Format a signature using Black.
@@ -144,12 +153,6 @@ def do_format_signature(
env = context.environment
# TODO: Stop using `do_get_template` when `*.html` templates are removed.
template = env.get_template(do_get_template(env, "signature"))
- config_annotations = context.parent["config"]["show_signature_annotations"]
- old_stash_ref_filter = env.filters["stash_crossref"]
-
- stash: dict[str, str] = {}
- if (annotations or config_annotations) and crossrefs:
- env.filters["stash_crossref"] = partial(_stash_crossref, stash)
if annotations is None:
new_context = context.parent
@@ -157,11 +160,8 @@ def do_format_signature(
new_context = dict(context.parent)
new_context["config"] = dict(new_context["config"])
new_context["config"]["show_signature_annotations"] = annotations
- try:
- signature = template.render(new_context, function=function, signature=True)
- finally:
- env.filters["stash_crossref"] = old_stash_ref_filter
+ signature = template.render(new_context, function=function, signature=True)
signature = _format_signature(callable_path, signature, line_length)
signature = str(
env.filters["highlight"](
@@ -181,9 +181,10 @@ def do_format_signature(
if signature.find('class="nf"') == -1:
signature = signature.replace('class="n"', 'class="nf"', 1)
- if stash:
+ if stash := env.filters["stash_crossref"].stash:
for key, value in stash.items():
signature = re.sub(rf"\b{key}\b", value, signature)
+ stash.clear()
return signature
@@ -195,7 +196,7 @@ def do_format_attribute(
attribute: Attribute,
line_length: int,
*,
- crossrefs: bool = False,
+ crossrefs: bool = False, # noqa: ARG001
) -> str:
"""Format an attribute using Black.
@@ -213,23 +214,14 @@ def do_format_attribute(
# TODO: Stop using `do_get_template` when `*.html` templates are removed.
template = env.get_template(do_get_template(env, "expression"))
annotations = context.parent["config"]["show_signature_annotations"]
- separate_signature = context.parent["config"]["separate_signature"]
- old_stash_ref_filter = env.filters["stash_crossref"]
- stash: dict[str, str] = {}
- if separate_signature and crossrefs:
- env.filters["stash_crossref"] = partial(_stash_crossref, stash)
-
- try:
- signature = str(attribute_path).strip()
- if annotations and attribute.annotation:
- annotation = template.render(context.parent, expression=attribute.annotation, signature=True)
- signature += f": {annotation}"
- if attribute.value:
- value = template.render(context.parent, expression=attribute.value, signature=True)
- signature += f" = {value}"
- finally:
- env.filters["stash_crossref"] = old_stash_ref_filter
+ signature = str(attribute_path).strip()
+ if annotations and attribute.annotation:
+ annotation = template.render(context.parent, expression=attribute.annotation, signature=True)
+ signature += f": {annotation}"
+ if attribute.value:
+ value = template.render(context.parent, expression=attribute.value, signature=True)
+ signature += f" = {value}"
signature = do_format_code(signature, line_length)
signature = str(
@@ -241,9 +233,10 @@ def do_format_attribute(
),
)
- if stash:
+ if stash := env.filters["stash_crossref"].stash:
for key, value in stash.items():
signature = re.sub(rf"\b{key}\b", value, signature)
+ stash.clear()
return signature
@@ -477,16 +470,7 @@ def do_get_template(env: Environment, obj: str | Object) -> str | Template:
template = env.get_template(f"{name}.html")
except TemplateNotFound:
return f"{name}.html.jinja"
- # TODO: Remove once support for Python 3.8 is dropped.
- if sys.version_info < (3, 9):
- try:
- Path(template.filename).relative_to(Path(__file__).parent) # type: ignore[arg-type]
- except ValueError:
- our_template = False
- else:
- our_template = True
- else:
- our_template = Path(template.filename).is_relative_to(Path(__file__).parent) # type: ignore[arg-type]
+ our_template = Path(template.filename).is_relative_to(Path(__file__).parent) # type: ignore[arg-type]
if our_template:
return f"{name}.html.jinja"
# TODO: Switch to a warning log after some time.
@@ -501,9 +485,9 @@ def do_get_template(env: Environment, obj: str | Object) -> str | Template:
@pass_context
def do_as_attributes_section(
context: Context, # noqa: ARG001
- attributes: Sequence[Attribute], # noqa: ARG001
+ attributes: Sequence[Attribute],
*,
- check_public: bool = True, # noqa: ARG001
+ check_public: bool = True,
) -> DocstringSectionAttributes:
"""Build an attributes section from a list of attributes.
@@ -514,15 +498,26 @@ def do_as_attributes_section(
Returns:
An attributes docstring section.
"""
- return DocstringSectionAttributes([])
+ return DocstringSectionAttributes(
+ [
+ DocstringAttribute(
+ name=attribute.name,
+ description=attribute.docstring.value.split("\n", 1)[0] if attribute.docstring else "",
+ annotation=attribute.annotation,
+ value=attribute.value, # type: ignore[arg-type]
+ )
+ for attribute in attributes
+ if not check_public or attribute.is_public
+ ],
+ )
@pass_context
def do_as_functions_section(
- context: Context, # noqa: ARG001
- functions: Sequence[Function], # noqa: ARG001
+ context: Context,
+ functions: Sequence[Function],
*,
- check_public: bool = True, # noqa: ARG001
+ check_public: bool = True,
) -> DocstringSectionFunctions:
"""Build a functions section from a list of functions.
@@ -533,15 +528,25 @@ def do_as_functions_section(
Returns:
A functions docstring section.
"""
- return DocstringSectionFunctions([])
+ keep_init_method = not context.parent["config"]["merge_init_into_class"]
+ return DocstringSectionFunctions(
+ [
+ DocstringFunction(
+ name=function.name,
+ description=function.docstring.value.split("\n", 1)[0] if function.docstring else "",
+ )
+ for function in functions
+ if (not check_public or function.is_public) and (function.name != "__init__" or keep_init_method)
+ ],
+ )
@pass_context
def do_as_classes_section(
context: Context, # noqa: ARG001
- classes: Sequence[Class], # noqa: ARG001
+ classes: Sequence[Class],
*,
- check_public: bool = True, # noqa: ARG001
+ check_public: bool = True,
) -> DocstringSectionClasses:
"""Build a classes section from a list of classes.
@@ -552,15 +557,24 @@ def do_as_classes_section(
Returns:
A classes docstring section.
"""
- return DocstringSectionClasses([])
+ return DocstringSectionClasses(
+ [
+ DocstringClass(
+ name=cls.name,
+ description=cls.docstring.value.split("\n", 1)[0] if cls.docstring else "",
+ )
+ for cls in classes
+ if not check_public or cls.is_public
+ ],
+ )
@pass_context
def do_as_modules_section(
context: Context, # noqa: ARG001
- modules: Sequence[Module], # noqa: ARG001
+ modules: Sequence[Module],
*,
- check_public: bool = True, # noqa: ARG001
+ check_public: bool = True,
) -> DocstringSectionModules:
"""Build a modules section from a list of modules.
@@ -571,7 +585,16 @@ def do_as_modules_section(
Returns:
A modules docstring section.
"""
- return DocstringSectionModules([])
+ return DocstringSectionModules(
+ [
+ DocstringModule(
+ name=module.name,
+ description=module.docstring.value.split("\n", 1)[0] if module.docstring else "",
+ )
+ for module in modules
+ if not check_public or module
+ ],
+ )
class AutorefsHook(AutorefsHookInterface):
@@ -589,7 +612,9 @@ def __init__(self, current_object: Object | Alias, config: dict[str, Any]) -> No
config: The configuration dictionary.
"""
self.current_object = current_object
+ """The current object being rendered."""
self.config = config
+ """The configuration options."""
def expand_identifier(self, identifier: str) -> str:
"""Expand an identifier.
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 fd13661b..24046e8f 100644
--- a/src/mkdocstrings_handlers/python/templates/material/_base/class.html.jinja
+++ b/src/mkdocstrings_handlers/python/templates/material/_base/class.html.jinja
@@ -142,6 +142,14 @@ Context:
{% endif %}
{% endblock docstring %}
+ {% block summary scoped %}
+ {#- Summary block.
+
+ This block renders auto-summaries for classes, methods, and attributes.
+ -#}
+ {% include "summary"|get_template with context %}
+ {% endblock summary %}
+
{% block source scoped %}
{#- Source block.
diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html.jinja b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html.jinja
index 8b0556f3..fef553b1 100644
--- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html.jinja
+++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html.jinja
@@ -34,7 +34,21 @@ Context:
{{ parameter.name }}
'|safe if config.show_symbol_type_toc else '') + parameter.name,
+ ) %}
+ {{ parameter.name }}
+ {% endfilter %}
+ {% else %}
+ {{ parameter.name }}
+ {% endif %}
+ {{ parameter.name }}
+ {% if config.parameter_headings %}
+ {% filter heading(
+ heading_level + 1,
+ role="param",
+ id=html_id ~ "(" ~ parameter.name ~ ")",
+ class="doc doc-heading doc-heading-parameter",
+ toc_label=('
'|safe if config.show_symbol_type_toc else '') + parameter.name,
+ ) %}
+ {{ parameter.name }}
+ {% endfilter %}
+ {% else %}
+ {{ parameter.name }}
+ {% endif %}
{% if parameter.annotation %}
{% with expression = parameter.annotation %}
({% include "expression"|get_template with context %}
@@ -100,7 +126,21 @@ Context:
{% for parameter in section.value %}
{{ parameter.name }}
'|safe if config.show_symbol_type_toc else '') + parameter.name,
+ ) %}
+ {{ parameter.name }}
+ {% endfilter %}
+ {% else %}
+ {{ parameter.name }}
+ {% endif %}
+