From 709aedc1345b9168074bf1b014cdd73a01726fb2 Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 13 Dec 2024 16:50:07 +0100 Subject: [PATCH 1/3] feat: Allow ruff to be used as a formatter --- src/mkdocstrings_handlers/python/rendering.py | 53 +++++++++++++++++-- tests/test_rendering.py | 14 +++-- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/mkdocstrings_handlers/python/rendering.py b/src/mkdocstrings_handlers/python/rendering.py index 5cbd5ebf..2fd257a7 100644 --- a/src/mkdocstrings_handlers/python/rendering.py +++ b/src/mkdocstrings_handlers/python/rendering.py @@ -3,9 +3,11 @@ from __future__ import annotations import enum +import importlib import random import re import string +import subprocess import sys import warnings from functools import lru_cache @@ -83,7 +85,7 @@ def do_format_code(code: str, line_length: int) -> str: code = code.strip() if len(code) < line_length: return code - formatter = _get_black_formatter() + formatter = _get_formatter() return formatter(code, line_length) @@ -118,7 +120,7 @@ def _format_signature(name: Markup, signature: str, line_length: int) -> str: # Black cannot format names with dots, so we replace # the whole name with a string of equal length name_length = len(name) - formatter = _get_black_formatter() + formatter = _get_formatter() formatable = f"def {'x' * name_length}{signature}: pass" formatted = formatter(formatable, line_length) @@ -434,12 +436,53 @@ def do_filter_objects( @lru_cache(maxsize=1) -def _get_black_formatter() -> Callable[[str, int], str]: +def _get_formatter() -> Callable[[str, int], str]: + for formatter_function in [ + _get_black_formatter, + _get_ruff_formatter, + ]: + if (formatter := formatter_function()) is not None: + return formatter + + logger.info("Formatting signatures requires either Black or ruff to be installed.") + return lambda text, _: text + + +@lru_cache(maxsize=1) +def _get_ruff_formatter() -> Callable[[str, int], str] | None: + if importlib.util.find_spec("ruff") is None: + return None + + def formatter(code: str, line_length: int) -> str: + try: + completed_process = subprocess.run( # noqa: S603 + [ # noqa: S607 + "ruff", + "format", + f'--config "line-length={line_length}"', + "--stdin-filename", + "file.py", + "-", + ], + check=True, + capture_output=True, + text=True, + input=code, + ) + except subprocess.CalledProcessError: + return code + else: + return completed_process.stdout + + return formatter + + +@lru_cache(maxsize=1) +def _get_black_formatter() -> Callable[[str, int], str] | None: try: from black import InvalidInput, Mode, format_str except ModuleNotFoundError: - logger.info("Formatting signatures requires Black to be installed.") - return lambda text, _: text + return None def formatter(code: str, line_length: int) -> str: mode = Mode(line_length=line_length) diff --git a/tests/test_rendering.py b/tests/test_rendering.py index 1bab29d7..56382cb8 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -4,7 +4,7 @@ import re from dataclasses import dataclass -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Callable import pytest from griffe import ModulesCollection, temporary_visited_module @@ -22,14 +22,22 @@ "aaaaa(bbbbb, ccccc=1) + ddddd.eeeee[ffff] or {ggggg: hhhhh, iiiii: jjjjj}", ], ) -def test_format_code(code: str) -> None: +@pytest.mark.parametrize( + "formatter", + [ + rendering._get_black_formatter(), + rendering._get_ruff_formatter(), + rendering._get_formatter(), + ], +) +def test_format_code(code: str, formatter: Callable[[str, int], str]) -> None: """Assert code can be Black-formatted. Parameters: code: Code to format. """ for length in (5, 100): - assert rendering.do_format_code(code, length) + assert formatter(code, length) @pytest.mark.parametrize( From 2c1ed32865d06a90e3f7e5cc7e80158dfe1daddb Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 13 Dec 2024 18:10:48 +0100 Subject: [PATCH 2/3] fix: Correctly calls `ruff` --- src/mkdocstrings_handlers/python/rendering.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/mkdocstrings_handlers/python/rendering.py b/src/mkdocstrings_handlers/python/rendering.py index 2fd257a7..bd7362d9 100644 --- a/src/mkdocstrings_handlers/python/rendering.py +++ b/src/mkdocstrings_handlers/python/rendering.py @@ -3,7 +3,6 @@ from __future__ import annotations import enum -import importlib import random import re import string @@ -448,18 +447,25 @@ def _get_formatter() -> Callable[[str, int], str]: return lambda text, _: text -@lru_cache(maxsize=1) def _get_ruff_formatter() -> Callable[[str, int], str] | None: - if importlib.util.find_spec("ruff") is None: + try: + from ruff.__main__ import find_ruff_bin + except ImportError: return None + try: + ruff_bin = find_ruff_bin() + except FileNotFoundError: + ruff_bin = "ruff" + def formatter(code: str, line_length: int) -> str: try: completed_process = subprocess.run( # noqa: S603 [ # noqa: S607 - "ruff", + ruff_bin, "format", - f'--config "line-length={line_length}"', + "--config", + f"line-length={line_length}", "--stdin-filename", "file.py", "-", @@ -477,7 +483,6 @@ def formatter(code: str, line_length: int) -> str: return formatter -@lru_cache(maxsize=1) def _get_black_formatter() -> Callable[[str, int], str] | None: try: from black import InvalidInput, Mode, format_str From 4fc2f4abe2e982357776e29911585923ac283745 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 16 Dec 2024 09:42:36 +0100 Subject: [PATCH 3/3] doc: Update documentation to account for Black/ruff --- docs/.glossary.md | 1 + docs/schema.json | 2 +- docs/usage/configuration/signatures.md | 18 ++++++++++++++---- src/mkdocstrings_handlers/python/handler.py | 2 +- src/mkdocstrings_handlers/python/rendering.py | 16 ++++++++-------- tests/test_rendering.py | 4 ++-- 6 files changed, 27 insertions(+), 16 deletions(-) diff --git a/docs/.glossary.md b/docs/.glossary.md index 588674fb..917c95c4 100644 --- a/docs/.glossary.md +++ b/docs/.glossary.md @@ -8,5 +8,6 @@ [Spacy's documentation]: https://spacy.io/api/doc/ [Black]: https://pypi.org/project/black/ [Material for MkDocs]: https://squidfunk.github.io/mkdocs-material +[Ruff]: https://docs.astral.sh/ruff *[ToC]: Table of Contents diff --git a/docs/schema.json b/docs/schema.json index b4eca004..e1863d26 100644 --- a/docs/schema.json +++ b/docs/schema.json @@ -145,7 +145,7 @@ "default": false }, "separate_signature": { - "title": "Whether to put the whole signature in a code block below the heading. If Black is installed, the signature is also formatted using it.", + "title": "Whether to put the whole signature in a code block below the heading. If a formatter (Black or Ruff) is installed, the signature is also formatted using it.", "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/signatures/#separate_signature", "type": "boolean", "default": false diff --git a/docs/usage/configuration/signatures.md b/docs/usage/configuration/signatures.md index e5e4cb88..879db6b1 100644 --- a/docs/usage/configuration/signatures.md +++ b/docs/usage/configuration/signatures.md @@ -154,10 +154,15 @@ def convert(text: str, md: Markdown) -> Markup: Maximum line length when formatting code/signatures. When separating signatures from headings with the [`separate_signature`][] option, -the Python handler will try to format the signatures using [Black] and +the Python handler will try to format the signatures using a formatter and the specified line length. -If Black is not installed, the handler issues an INFO log once. +The handler will automatically try to format using : + +1. [Black] +2. [Ruff] + +If a formatter is not found, the handler issues an INFO log once. ```yaml title="in mkdocs.yml (global configuration)" plugins: @@ -380,10 +385,15 @@ function(param1, param2=None) Whether to put the whole signature in a code block below the heading. When separating signatures from headings, -the Python handler will try to format the signatures using [Black] and +the Python handler will try to format the signatures using a formatter and the specified [line length][line_length]. -If Black is not installed, the handler issues an INFO log once. +The handler will automatically try to format using : + +1. [Black] +2. [Ruff] + +If a formatter is not found, the handler issues an INFO log once. ```yaml title="in mkdocs.yml (global configuration)" plugins: diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 628b56ec..4171fd76 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -201,7 +201,7 @@ class PythonHandler(BaseHandler): 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`. + If a formatter (Black or Ruff) 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`. """ diff --git a/src/mkdocstrings_handlers/python/rendering.py b/src/mkdocstrings_handlers/python/rendering.py index bd7362d9..085f0c34 100644 --- a/src/mkdocstrings_handlers/python/rendering.py +++ b/src/mkdocstrings_handlers/python/rendering.py @@ -72,11 +72,11 @@ def _sort_key_source(item: CollectorItem) -> Any: def do_format_code(code: str, line_length: int) -> str: - """Format code using Black. + """Format code. Parameters: code: The code to format. - line_length: The line length to give to Black. + line_length: The line length. Returns: The same code, formatted. @@ -138,13 +138,13 @@ def do_format_signature( annotations: bool | None = None, crossrefs: bool = False, # noqa: ARG001 ) -> str: - """Format a signature using Black. + """Format a signature. Parameters: context: Jinja context, passed automatically. callable_path: The path of the callable we render the signature of. function: The function we render the signature of. - line_length: The line length to give to Black. + line_length: The line length. annotations: Whether to show type annotations. crossrefs: Whether to cross-reference types in the signature. @@ -200,13 +200,13 @@ def do_format_attribute( *, crossrefs: bool = False, # noqa: ARG001 ) -> str: - """Format an attribute using Black. + """Format an attribute. Parameters: context: Jinja context, passed automatically. attribute_path: The path of the callable we render the signature of. attribute: The attribute we render the signature of. - line_length: The line length to give to Black. + line_length: The line length. crossrefs: Whether to cross-reference types in the signature. Returns: @@ -443,7 +443,7 @@ def _get_formatter() -> Callable[[str, int], str]: if (formatter := formatter_function()) is not None: return formatter - logger.info("Formatting signatures requires either Black or ruff to be installed.") + logger.info("Formatting signatures requires either Black or Ruff to be installed.") return lambda text, _: text @@ -461,7 +461,7 @@ def _get_ruff_formatter() -> Callable[[str, int], str] | None: def formatter(code: str, line_length: int) -> str: try: completed_process = subprocess.run( # noqa: S603 - [ # noqa: S607 + [ ruff_bin, "format", "--config", diff --git a/tests/test_rendering.py b/tests/test_rendering.py index 56382cb8..081702f4 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -31,7 +31,7 @@ ], ) def test_format_code(code: str, formatter: Callable[[str, int], str]) -> None: - """Assert code can be Black-formatted. + """Assert code can be formatted. Parameters: code: Code to format. @@ -45,7 +45,7 @@ def test_format_code(code: str, formatter: Callable[[str, int], str]) -> None: [("Class.method", "(param: str = 'hello') -> 'OtherClass'")], ) def test_format_signature(name: Markup, signature: str) -> None: - """Assert signatures can be Black-formatted. + """Assert signatures can be formatted. Parameters: signature: Signature to format.