From 3b9dba2709a8668e379c6ce1536cb1714971b3f4 Mon Sep 17 00:00:00 2001 From: James McDonnell <447668+ElementalWarrior@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:02:10 -0700 Subject: [PATCH 001/158] refactor: Declare default CSS symbol colors under :host as well This is apparently needed when the docs are served under a shadow DOM (for example, Backstage seems to do this, and `:root` variables aren't picked up). See https://developer.mozilla.org/en-US/docs/Web/CSS/:host. PR-186: https://github.com/mkdocstrings/python/pull/186 --- src/mkdocstrings_handlers/python/templates/material/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocstrings_handlers/python/templates/material/style.css b/src/mkdocstrings_handlers/python/templates/material/style.css index 154be85d..9547fa5a 100644 --- a/src/mkdocstrings_handlers/python/templates/material/style.css +++ b/src/mkdocstrings_handlers/python/templates/material/style.css @@ -31,7 +31,7 @@ } /* Symbols in Navigation and ToC. */ -:root, +:root, :host, [data-md-color-scheme="default"] { --doc-symbol-attribute-fg-color: #953800; --doc-symbol-function-fg-color: #8250df; From cbdf29441b192631031070305268ac884f5b1edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 12 Oct 2024 17:59:18 +0200 Subject: [PATCH 002/158] chore: Template upgrade --- .copier-answers.yml | 2 +- .github/workflows/ci.yml | 29 ++++++---- .github/workflows/release.yml | 20 +++---- .gitignore | 1 + .gitpod.dockerfile | 6 -- .gitpod.yml | 13 ----- CONTRIBUTING.md | 5 +- config/ruff.toml | 2 +- devdeps.txt | 32 ----------- duties.py | 22 +++++-- mkdocs.yml | 5 +- pyproject.toml | 41 +++++++++++++- scripts/gen_credits.py | 12 ++-- scripts/insiders.py | 5 +- scripts/make | 104 ++++++++++++++-------------------- 15 files changed, 142 insertions(+), 157 deletions(-) delete mode 100644 .gitpod.dockerfile delete mode 100644 .gitpod.yml delete mode 100644 devdeps.txt 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/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/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..2d5640c4 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,12 +17,12 @@ 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 :: Utilities", @@ -58,7 +58,6 @@ source-includes = [ "scripts", "share", "tests", - "devdeps.txt", "duties.py", "mkdocs.yml", "*.md", @@ -69,3 +68,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: From 6615c91cdc035bc0c2fdd12f3952ff84f5e1c04e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 12 Oct 2024 18:06:09 +0200 Subject: [PATCH 003/158] build: Drop support for Python 3.8 --- src/mkdocstrings_handlers/python/handler.py | 4 +++- src/mkdocstrings_handlers/python/rendering.py | 16 +++++----------- tests/conftest.py | 3 ++- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index ef93ee3b..83315d10 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 diff --git a/src/mkdocstrings_handlers/python/rendering.py b/src/mkdocstrings_handlers/python/rendering.py index 2c4a4893..09be07ca 100644 --- a/src/mkdocstrings_handlers/python/rendering.py +++ b/src/mkdocstrings_handlers/python/rendering.py @@ -10,7 +10,8 @@ import warnings from functools import lru_cache, partial 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 from griffe import ( Alias, @@ -26,6 +27,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 @@ -477,16 +480,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. diff --git a/tests/conftest.py b/tests/conftest.py index f7b28105..88105e4c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,13 +3,14 @@ from __future__ import annotations from collections import ChainMap -from typing import TYPE_CHECKING, Any, Iterator +from typing import TYPE_CHECKING, Any import pytest from markdown.core import Markdown from mkdocs.config.defaults import MkDocsConfig if TYPE_CHECKING: + from collections.abc import Iterator from pathlib import Path from mkdocs import config From 0176b83f21ae02d345489c93cca3baf51f8bc58c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 12 Oct 2024 18:17:19 +0200 Subject: [PATCH 004/158] feat: Parameter headings, more automatic cross-references --- src/mkdocstrings_handlers/python/handler.py | 4 +- src/mkdocstrings_handlers/python/rendering.py | 72 ++++++++----------- .../_base/docstring/parameters.html.jinja | 46 +++++++++++- .../material/_base/expression.html.jinja | 34 ++++++++- .../material/_base/signature.html.jinja | 47 ++++++++++-- .../python/templates/material/style.css | 24 +++++++ 6 files changed, 173 insertions(+), 54 deletions(-) diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 83315d10..aa690bca 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -118,6 +118,7 @@ class PythonHandler(BaseHandler): "summary": False, "show_labels": True, "unwrap_annotated": False, + "parameter_headings": False, } """Default handler configuration. @@ -138,6 +139,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`. @@ -426,7 +428,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 09be07ca..9c01f172 100644 --- a/src/mkdocstrings_handlers/python/rendering.py +++ b/src/mkdocstrings_handlers/python/rendering.py @@ -83,24 +83,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]] = {} + @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_key(length: int) -> str: - return "_" + "".join(random.choice(_stash_key_alphabet) 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 __call__(self, crossref: str, *, length: int) -> str: + key = self._gen_stash_key(length) + self.stash[key] = crossref + 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 _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: @@ -129,7 +131,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. @@ -147,12 +149,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 @@ -160,11 +156,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"]( @@ -184,9 +177,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 @@ -198,7 +192,7 @@ def do_format_attribute( attribute: Attribute, line_length: int, *, - crossrefs: bool = False, + crossrefs: bool = False, # noqa: ARG001 ) -> str: """Format an attribute using Black. @@ -216,23 +210,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( @@ -244,9 +229,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 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: {% for parameter in section.value %} - {{ 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 %} @@ -68,7 +82,19 @@ Context: