diff --git a/.copier-answers.yml b/.copier-answers.yml index 70fc1c32..2076d959 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 0.15.2 +_commit: 0.15.6 _src_path: gh:pawamoy/copier-pdm author_email: pawamoy@pm.me author_fullname: Timothée Mazzucotelli diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00d61acb..8c0977e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,10 +31,10 @@ jobs: python-version: "3.8" - name: Resolving dependencies - run: pdm lock -v + run: pdm lock -v --no-cross-platform -G ci-quality - name: Install dependencies - run: pdm install -G duty -G docs -G quality -G typing -G security + run: pdm install -G ci-quality - name: Check if the documentation builds correctly run: pdm run duty check-docs @@ -54,6 +54,7 @@ jobs: tests: strategy: + max-parallel: 4 matrix: os: - ubuntu-latest @@ -78,10 +79,10 @@ jobs: python-version: ${{ matrix.python-version }} - name: Resolving dependencies - run: pdm lock -v + run: pdm lock -v --no-cross-platform -G ci-tests - name: Install dependencies - run: pdm install --no-editable -G duty -G tests -G docs + run: pdm install --no-editable -G ci-tests - name: Run the test suite run: pdm run duty test diff --git a/.gitignore b/.gitignore index 428b2409..ae47b28b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ pip-wheel-metadata/ site/ pdm.lock pdm.toml +.pdm-plugins/ .pdm-python __pypackages__/ .venv/ diff --git a/CHANGELOG.md b/CHANGELOG.md index b9606a25..d4b4606c 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.0.0](https://github.com/mkdocstrings/python/releases/tag/1.0.0) - 2023-05-11 + +[Compare with 0.10.1](https://github.com/mkdocstrings/python/compare/0.10.1...1.0.0) + +### Breaking changes + +- The signature of the [`format_signature` filter](https://mkdocstrings.github.io/python/reference/mkdocstrings_handlers/python/rendering/#mkdocstrings_handlers.python.rendering.do_format_signature) has changed. + If you override templates in your project to customize the output, + make sure to update the following templates so that they use + the new filter signature: + + - `class.html` + - `expression.html` + - `function.html` + - `signature.html` + + You can see how to use the filter in this commit's changes: + [f686f4e4](https://github.com/mkdocstrings/python/commit/f686f4e4599cea64686d4ef4863b507dd096a513). + +**We take this as an opportunity to go out of beta and bump the version to 1.0.0. +This will allow users to rely on semantic versioning.** + +### Bug Fixes + +- Bring compatibility with insiders signature crossrefs feature ([f686f4e](https://github.com/mkdocstrings/python/commit/f686f4e4599cea64686d4ef4863b507dd096a513) by Timothée Mazzucotelli). + ## [0.10.1](https://github.com/mkdocstrings/python/releases/tag/0.10.1) - 2023-05-07 [Compare with 0.10.0](https://github.com/mkdocstrings/python/compare/0.10.0...0.10.1) diff --git a/Makefile b/Makefile index 68fea02f..1b33c56b 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ help: .PHONY: lock lock: - @pdm lock + @pdm lock -G:all .PHONY: setup setup: diff --git a/docs/.overrides/main.html b/docs/.overrides/main.html index cb5234e5..cf8adeb7 100644 --- a/docs/.overrides/main.html +++ b/docs/.overrides/main.html @@ -6,9 +6,9 @@ is now available! {% include ".icons/octicons/heart-fill-16.svg" %} - + — - — For updates follow @pawamoy on + For updates follow @pawamoy on {% include ".icons/fontawesome/brands/mastodon.svg" %} diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css index 87842c0c..559575fb 100644 --- a/docs/css/mkdocstrings.css +++ b/docs/css/mkdocstrings.css @@ -4,7 +4,7 @@ div.doc-contents:not(.first) { border-left: .05rem solid var(--md-typeset-table-color); } -/* Mark external links as such */ +/* Mark external links as such. */ a.external::after, a.autorefs-external::after { /* https://primer.style/octicons/arrow-up-right-24 */ @@ -12,13 +12,14 @@ a.autorefs-external::after { content: ' '; display: inline-block; + vertical-align: middle; position: relative; - top: 0.1em; + bottom: 0.1em; margin-left: 0.2em; margin-right: 0.1em; - height: 1em; - width: 1em; + height: 0.7em; + width: 0.7em; border-radius: 100%; background-color: var(--md-typeset-a-color); } diff --git a/docs/usage/customization.md b/docs/usage/customization.md index dd2bd56c..99b33bd2 100644 --- a/docs/usage/customization.md +++ b/docs/usage/customization.md @@ -70,20 +70,20 @@ a.autorefs-external::after { content: ' '; display: inline-block; + vertical-align: middle; position: relative; - top: 0.1em; + bottom: 0.1em; margin-left: 0.2em; margin-right: 0.1em; - height: 1em; - width: 1em; + height: 0.7em; + width: 0.7em; border-radius: 100%; background-color: var(--md-typeset-a-color); } a.autorefs-external:hover::after { background-color: var(--md-accent-fg-color); } - ``` ### Recommended style (ReadTheDocs) diff --git a/duties.py b/duties.py index 9b93ca81..a49b49cb 100644 --- a/duties.py +++ b/duties.py @@ -5,7 +5,7 @@ import os import sys from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from duty import duty from duty.callables import black, blacken_docs, coverage, lazy, mkdocs, mypy, pytest, ruff, safety @@ -35,7 +35,29 @@ def pyprefix(title: str) -> str: # noqa: D103 return title +def merge(d1: Any, d2: Any) -> Any: # noqa: D103 + basic_types = (int, float, str, bool, complex) + if isinstance(d1, dict) and isinstance(d2, dict): + for key, value in d2.items(): + if key in d1: + if isinstance(d1[key], basic_types): + d1[key] = value + else: + d1[key] = merge(d1[key], value) + else: + d1[key] = value + return d1 + if isinstance(d1, list) and isinstance(d2, list): + return d1 + d2 + return d2 + + def mkdocs_config() -> str: # noqa: D103 + from mkdocs import utils + + # patch YAML loader to merge arrays + utils.merge = merge + if "+insiders" in pkgversion("mkdocs-material"): return "mkdocs.insiders.yml" return "mkdocs.yml" @@ -83,6 +105,7 @@ def check_quality(ctx: Context) -> None: Parameters: ctx: The context instance (passed automatically). """ + os.environ["MYPYPATH"] = "src" ctx.run( ruff.check(*PY_SRC_LIST, config="config/ruff.toml"), title=pyprefix("Checking code quality"), diff --git a/mkdocs.insiders.yml b/mkdocs.insiders.yml index 93e3a93b..9afba9aa 100644 --- a/mkdocs.insiders.yml +++ b/mkdocs.insiders.yml @@ -1,4 +1,4 @@ INHERIT: mkdocs.yml plugins: - typeset: {} +- typeset diff --git a/mkdocs.yml b/mkdocs.yml index bdb863e9..50ba6925 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -119,16 +119,16 @@ markdown_extensions: permalink: "¤" plugins: - autorefs: {} - search: {} - markdown-exec: {} - gen-files: +- autorefs +- search +- markdown-exec +- gen-files: scripts: - scripts/gen_ref_nav.py - literate-nav: +- literate-nav: nav_file: SUMMARY.txt - coverage: {} - mkdocstrings: +- coverage +- mkdocstrings: handlers: python: paths: [src] @@ -141,10 +141,10 @@ plugins: merge_init_into_class: true docstring_options: ignore_init_summary: true - git-committers: +- git-committers: enabled: !ENV [DEPLOY, false] repository: mkdocstrings/python - minify: +- minify: minify_html: !ENV [DEPLOY, false] extra: diff --git a/pyproject.toml b/pyproject.toml index 22d865ae..84f0f2f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,9 @@ Funding = "https://github.com/sponsors/mkdocstrings" [tool.pdm] version = {source = "scm"} +plugins = [ + "pdm-multirun", +] [tool.pdm.build] package-dir = "src" @@ -53,6 +56,8 @@ editable-backend = "editables" [tool.pdm.dev-dependencies] duty = ["duty>=0.10"] +ci-quality = ["mkdocstrings-python[duty,docs,quality,typing,security]"] +ci-tests = ["mkdocstrings-python[duty,docs,tests]"] docs = [ "black>=23.1", "markdown-callouts>=0.2", @@ -86,4 +91,6 @@ typing = [ "types-pyyaml>=6.0", "types-toml>=0.10", ] -security = ["safety>=2"] +security = [ + "safety>=2", +] diff --git a/scripts/setup.sh b/scripts/setup.sh index 559ae8b1..a0f87210 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -10,7 +10,7 @@ if ! command -v pdm &>/dev/null; then pipx install pdm fi if ! pdm self list 2>/dev/null | grep -q pdm-multirun; then - pipx inject pdm pdm-multirun + pdm install --plugins fi if [ -n "${PYTHON_VERSIONS}" ]; then diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 775d7de0..9bfb02f4 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -84,6 +84,7 @@ class PythonHandler(BaseHandler): "show_if_no_docstring": False, "show_signature": True, "show_signature_annotations": False, + "signature_crossrefs": False, "separate_signature": False, "line_length": 60, "merge_init_into_class": False, @@ -169,6 +170,7 @@ class PythonHandler(BaseHandler): line_length (int): Maximum line length when formatting code/signatures. Default: `60`. show_signature (bool): Show methods and functions signatures. Default: `True`. show_signature_annotations (bool): Show the type annotations in methods and functions signatures. Default: `False`. + signature_crossrefs (bool): Whether to render cross-references for type annotations in signatures. Default: `False`. 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`. """ @@ -314,6 +316,9 @@ def render(self, data: CollectorItem, config: Mapping[str, Any]) -> str: # noqa (re.compile(filtr.lstrip("!")), filtr.startswith("!")) for filtr in final_config["filters"] ] + # TODO: goal reached: remove once `signature_crossrefs` feature becomes public + final_config["signature_crossrefs"] = False + return template.render( **{"config": final_config, data.kind.value: data, "heading_level": heading_level, "root": True}, ) @@ -329,6 +334,7 @@ def update_env(self, md: Markdown, config: dict) -> None: # noqa: D102 (ignore self.env.filters["format_code"] = rendering.do_format_code self.env.filters["format_signature"] = rendering.do_format_signature self.env.filters["filter_objects"] = rendering.do_filter_objects + self.env.filters["stash_crossref"] = lambda ref, length: ref def get_anchors(self, data: CollectorItem) -> set[str]: # noqa: D102 (ignore missing docstring) try: diff --git a/src/mkdocstrings_handlers/python/rendering.py b/src/mkdocstrings_handlers/python/rendering.py index 7edfd7ac..d1f0eb75 100644 --- a/src/mkdocstrings_handlers/python/rendering.py +++ b/src/mkdocstrings_handlers/python/rendering.py @@ -8,11 +8,13 @@ from functools import lru_cache from typing import TYPE_CHECKING, Any, Callable, Match, Pattern, Sequence +from jinja2 import pass_context from markupsafe import Markup from mkdocstrings.loggers import get_logger if TYPE_CHECKING: - from griffe.dataclasses import Alias, Object + from griffe.dataclasses import Alias, Function, Object + from jinja2.runtime import Context from mkdocstrings.handlers.base import CollectorItem logger = get_logger(__name__) @@ -60,26 +62,17 @@ def do_format_code(code: str, line_length: int) -> str: return formatter(code, line_length) -def do_format_signature(signature: str, line_length: int) -> str: - """Format a signature using Black. - - Parameters: - signature: The signature to format. - line_length: The line length to give to Black. - - Returns: - The same code, formatted. - """ - code = signature.strip() - if len(code) < line_length: - return code +def _format_signature(name: Markup, signature: str, line_length: int) -> str: + name = str(name).strip() # type: ignore[assignment] + signature = signature.strip() + if len(name + signature) < line_length: + return name + signature # Black cannot format names with dots, so we replace # the whole name with a string of equal length - name_length = code.index("(") - name = code[:name_length] + name_length = len(name) formatter = _get_black_formatter() - formatable = f"def {'x' * name_length}{code[name_length:]}: pass" + formatable = f"def {'x' * name_length}{signature}: pass" formatted = formatter(formatable, line_length) # We put back the original name @@ -87,6 +80,33 @@ def do_format_signature(signature: str, line_length: int) -> str: return name + formatted[4:-5].strip()[name_length:-1] +@pass_context +def do_format_signature( + context: Context, + callable_path: Markup, + function: Function, + line_length: int, + *, + crossrefs: bool = False, # noqa: ARG001 +) -> str: + """Format a signature using Black. + + Parameters: + callable_path: The path of the callable we render the signature of. + line_length: The line length to give to Black. + crossrefs: Whether to cross-reference types in the signature. + + Returns: + The same code, formatted. + """ + env = context.environment + template = env.get_template("signature.html") + signature = template.render(context.parent, function=function) + signature = _format_signature(callable_path, signature, line_length) + signature = str(env.filters["highlight"](signature, language="python", inline=False)) + return signature + + def do_order_members( members: Sequence[Object | Alias], order: Order, diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/class.html b/src/mkdocstrings_handlers/python/templates/material/_base/class.html index ff102c88..3f6248fb 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/class.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/class.html @@ -43,11 +43,8 @@ {% if config.separate_signature and config.merge_init_into_class %} {% if "__init__" in class.members %} {% with function = class.members["__init__"] %} - {% filter highlight(language="python", inline=False) %} - {% filter format_signature(config.line_length) %} - {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} - {% include "signature.html" with context %} - {% endfilter %} + {% filter format_signature(function, config.line_length, crossrefs=config.signature_crossrefs) %} + {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} {% endfilter %} {% endwith %} {% endif %} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/expression.html b/src/mkdocstrings_handlers/python/templates/material/_base/expression.html index 3347e272..9bcfc867 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/expression.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/expression.html @@ -7,6 +7,8 @@ {{ original_expression }} {%- else -%} {%- with annotation = original_expression|attr(config.annotations_path) -%} - {{ annotation }} + {%- filter stash_crossref(length=annotation|length) -%} + {{ annotation }} + {%- endfilter -%} {%- endwith -%} {%- endif -%} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/function.html b/src/mkdocstrings_handlers/python/templates/material/_base/function.html index b9b1696c..70c26892 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/function.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/function.html @@ -37,11 +37,8 @@ {% endfilter %} {% if config.separate_signature %} - {% filter highlight(language="python", inline=False) %} - {% filter format_signature(config.line_length) %} - {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} - {% include "signature.html" with context %} - {% endfilter %} + {% filter format_signature(function, config.line_length, crossrefs=config.signature_crossrefs) %} + {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} {% endfilter %} {% endif %} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/signature.html b/src/mkdocstrings_handlers/python/templates/material/_base/signature.html index bc24ea35..ea642f51 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/signature.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/signature.html @@ -2,7 +2,14 @@ {{ log.debug("Rendering signature") }} {%- with -%} - {%- set ns = namespace(has_pos_only=False, render_pos_only_separator=True, render_kw_only_separator=True, equal="=") -%} + {%- set ns = namespace( + has_pos_only=False, + render_pos_only_separator=True, + render_kw_only_separator=True, + annotation="", + equal="=", + ) + -%} {%- if config.show_signature_annotations -%} {%- set ns.equal = " = " -%} @@ -24,7 +31,13 @@ {%- endif -%} {%- if config.show_signature_annotations and parameter.annotation is not none -%} - {%- set annotation = ": " + parameter.annotation|safe -%} + {%- if config.separate_signature and config.signature_crossrefs -%} + {%- with expression = parameter.annotation -%} + {%- set ns.annotation -%}: {% include "expression.html" with context %}{%- endset -%} + {%- endwith -%} + {%- else -%} + {%- set ns.annotation = ": " + parameter.annotation|safe -%} + {%- endif -%} {%- endif -%} {%- if parameter.default is not none and parameter.kind.value != "variadic positional" and parameter.kind.value != "variadic keyword" -%} @@ -36,13 +49,18 @@ {%- endif -%} {% if parameter.kind.value == "variadic positional" %}*{% elif parameter.kind.value == "variadic keyword" %}**{% endif -%} - {{ parameter.name }}{{ annotation }}{{ default }} + {{ parameter.name }}{{ ns.annotation }}{{ default }} {%- if not loop.last %}, {% endif -%} {%- endif -%} {%- endfor -%} ) - {%- if config.show_signature_annotations and function.annotation %} -> {{ function.annotation|safe }}{%- endif -%} + {%- if config.show_signature_annotations and function.annotation %} -> {% if config.separate_signature and config.signature_crossrefs -%} + {%- with expression = function.annotation %}{% include "expression.html" with context %}{%- endwith -%} + {%- else -%} + {{ function.annotation|safe }} + {%- endif -%} + {%- endif -%} {%- endwith -%} {%- endif -%} \ No newline at end of file diff --git a/tests/test_rendering.py b/tests/test_rendering.py index fc970c57..c504d4d0 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -4,12 +4,15 @@ import re from dataclasses import dataclass -from typing import Any +from typing import TYPE_CHECKING, Any import pytest from mkdocstrings_handlers.python import rendering +if TYPE_CHECKING: + from markupsafe import Markup + @pytest.mark.parametrize( "code", @@ -29,17 +32,17 @@ def test_format_code(code: str) -> None: @pytest.mark.parametrize( - "signature", - ["Class.method(param: str = 'hello') -> 'OtherClass'"], + ("name", "signature"), + [("Class.method", "(param: str = 'hello') -> 'OtherClass'")], ) -def test_format_signature(signature: str) -> None: +def test_format_signature(name: Markup, signature: str) -> None: """Assert signatures can be Black-formatted. Parameters: signature: Signature to format. """ for length in (5, 100): - assert rendering.do_format_signature(signature, length) + assert rendering._format_signature(name, signature, length) @dataclass