From ab3648c4c58871ee111be787a3843d7e6842d385 Mon Sep 17 00:00:00 2001 From: Calum Young <32770960+calumy@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:55:10 +0100 Subject: [PATCH] Format docs with ruff formatter (#13087) ## Summary Now that Ruff provides a formatter, there is no need to rely on Black to check that the docs are formatted correctly in `check_docs_formatted.py`. This PR swaps out Black for the Ruff formatter and updates inconsistencies between the two. This PR will be a precursor to another PR ([branch](https://github.com/calumy/ruff/tree/format-pyi-in-docs)), updating the `check_docs_formatted.py` script to check for pyi files, fixing #11568. ## Test Plan - CI to check that the docs are formatted correctly using the updated script. --- .../pylint/rules/invalid_string_characters.rs | 6 +- docs/requirements.txt | 2 +- scripts/check_docs_formatted.py | 64 ++++++++++++------- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs index eb51ad0507..16a0b8ba79 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs @@ -57,7 +57,7 @@ impl AlwaysFixableViolation for InvalidCharacterBackspace { /// /// Use instead: /// ```python -/// x = "\x1A" +/// x = "\x1a" /// ``` #[violation] pub struct InvalidCharacterSub; @@ -90,7 +90,7 @@ impl AlwaysFixableViolation for InvalidCharacterSub { /// /// Use instead: /// ```python -/// x = "\x1B" +/// x = "\x1b" /// ``` #[violation] pub struct InvalidCharacterEsc; @@ -155,7 +155,7 @@ impl AlwaysFixableViolation for InvalidCharacterNul { /// /// Use instead: /// ```python -/// x = "Dear Sir\u200B/\u200BMadam" # zero width space +/// x = "Dear Sir\u200b/\u200bMadam" # zero width space /// ``` #[violation] pub struct InvalidCharacterZeroWidthSpace; diff --git a/docs/requirements.txt b/docs/requirements.txt index 2ab4ae5e2d..f8daa571df 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ PyYAML==6.0.2 -black==24.8.0 +ruff==0.6.2 mkdocs==1.6.0 mkdocs-material==9.1.18 mkdocs-redirects==1.2.1 diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index 64d3dbe146..4a3a70ca5e 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -6,19 +6,15 @@ from __future__ import annotations import argparse import os import re +import subprocess import textwrap from pathlib import Path from re import Match from typing import TYPE_CHECKING -import black -from black.mode import Mode, TargetVersion -from black.parsing import InvalidInput - if TYPE_CHECKING: from collections.abc import Sequence -TARGET_VERSIONS = ["py37", "py38", "py39", "py310", "py311"] SNIPPED_RE = re.compile( r"(?P^(?P *)```\s*python\n)" r"(?P.*?)" @@ -52,6 +48,7 @@ KNOWN_FORMATTING_VIOLATIONS = [ "missing-whitespace-around-modulo-operator", "missing-whitespace-around-operator", "missing-whitespace-around-parameter-equals", + "module-import-not-at-top-of-file", "multi-line-implicit-string-concatenation", "multiple-leading-hashes-for-block-comment", "multiple-spaces-after-comma", @@ -119,19 +116,46 @@ class CodeBlockError(Exception): """A code block parse error.""" -def format_str( - src: str, - black_mode: black.FileMode, -) -> tuple[str, Sequence[CodeBlockError]]: - """Format a single docs file string.""" +class InvalidInput(ValueError): + """Raised when ruff fails to parse file.""" + + +def format_str(code: str) -> str: + """Format a code block with ruff by writing to a temporary file.""" + # Run ruff to format the tmp file + try: + completed_process = subprocess.run( + ["ruff", "format", "-"], + check=True, + capture_output=True, + text=True, + input=code, + ) + except subprocess.CalledProcessError as e: + err = e.stderr + if "error: Failed to parse" in err: + raise InvalidInput(err) from e + + raise NotImplementedError( + "This error has not been handled correctly, please update " + f"`check_docs_formatted.py\n\nError:\n\n{err}", + ) from e + + return completed_process.stdout + + +def format_contents(src: str) -> tuple[str, Sequence[CodeBlockError]]: + """Format a single docs content.""" errors: list[CodeBlockError] = [] def _snipped_match(match: Match[str]) -> str: code = textwrap.dedent(match["code"]) try: - code = black.format_str(code, mode=black_mode) + code = format_str(code) except InvalidInput as e: errors.append(CodeBlockError(e)) + except NotImplementedError as e: + raise e code = textwrap.indent(code, match["indent"]) return f'{match["before"]}{code}{match["after"]}' @@ -140,12 +164,7 @@ def format_str( return src, errors -def format_file( - file: Path, - black_mode: black.FileMode, - error_known: bool, - args: argparse.Namespace, -) -> int: +def format_file(file: Path, error_known: bool, args: argparse.Namespace) -> int: """Check the formatting of a single docs file. Returns the exit code for the script. @@ -170,7 +189,7 @@ def format_file( # Remove everything after the last example contents = contents[: contents.rfind("```")] + "```" - new_contents, errors = format_str(contents, black_mode) + new_contents, errors = format_contents(contents) if errors and not args.skip_errors and not error_known: for error in errors: @@ -237,10 +256,6 @@ def main(argv: Sequence[str] | None = None) -> int: print("Please generate rules first.") return 1 - black_mode = Mode( - target_versions={TargetVersion[val.upper()] for val in TARGET_VERSIONS}, - ) - # Check known formatting violations and parse errors are sorted alphabetically and # have no duplicates. This will reduce the diff when adding new violations @@ -264,6 +279,7 @@ def main(argv: Sequence[str] | None = None) -> int: violations = 0 errors = 0 + print("Checking docs formatting...") for file in [*static_docs, *generated_docs]: rule_name = file.name.split(".")[0] if rule_name in KNOWN_FORMATTING_VIOLATIONS: @@ -271,7 +287,7 @@ def main(argv: Sequence[str] | None = None) -> int: error_known = rule_name in KNOWN_PARSE_ERRORS - result = format_file(file, black_mode, error_known, args) + result = format_file(file, error_known, args) if result == 1: violations += 1 elif result == 2 and not error_known: @@ -286,6 +302,8 @@ def main(argv: Sequence[str] | None = None) -> int: if violations > 0 or errors > 0: return 1 + print("All docs are formatted correctly.") + return 0