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.
This commit is contained in:
Calum Young 2024-08-26 16:55:10 +01:00 committed by GitHub
parent a822fd6642
commit ab3648c4c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 45 additions and 27 deletions

View file

@ -57,7 +57,7 @@ impl AlwaysFixableViolation for InvalidCharacterBackspace {
/// ///
/// Use instead: /// Use instead:
/// ```python /// ```python
/// x = "\x1A" /// x = "\x1a"
/// ``` /// ```
#[violation] #[violation]
pub struct InvalidCharacterSub; pub struct InvalidCharacterSub;
@ -90,7 +90,7 @@ impl AlwaysFixableViolation for InvalidCharacterSub {
/// ///
/// Use instead: /// Use instead:
/// ```python /// ```python
/// x = "\x1B" /// x = "\x1b"
/// ``` /// ```
#[violation] #[violation]
pub struct InvalidCharacterEsc; pub struct InvalidCharacterEsc;
@ -155,7 +155,7 @@ impl AlwaysFixableViolation for InvalidCharacterNul {
/// ///
/// Use instead: /// Use instead:
/// ```python /// ```python
/// x = "Dear Sir\u200B/\u200BMadam" # zero width space /// x = "Dear Sir\u200b/\u200bMadam" # zero width space
/// ``` /// ```
#[violation] #[violation]
pub struct InvalidCharacterZeroWidthSpace; pub struct InvalidCharacterZeroWidthSpace;

View file

@ -1,5 +1,5 @@
PyYAML==6.0.2 PyYAML==6.0.2
black==24.8.0 ruff==0.6.2
mkdocs==1.6.0 mkdocs==1.6.0
mkdocs-material==9.1.18 mkdocs-material==9.1.18
mkdocs-redirects==1.2.1 mkdocs-redirects==1.2.1

View file

@ -6,19 +6,15 @@ from __future__ import annotations
import argparse import argparse
import os import os
import re import re
import subprocess
import textwrap import textwrap
from pathlib import Path from pathlib import Path
from re import Match from re import Match
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import black
from black.mode import Mode, TargetVersion
from black.parsing import InvalidInput
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Sequence from collections.abc import Sequence
TARGET_VERSIONS = ["py37", "py38", "py39", "py310", "py311"]
SNIPPED_RE = re.compile( SNIPPED_RE = re.compile(
r"(?P<before>^(?P<indent> *)```\s*python\n)" r"(?P<before>^(?P<indent> *)```\s*python\n)"
r"(?P<code>.*?)" r"(?P<code>.*?)"
@ -52,6 +48,7 @@ KNOWN_FORMATTING_VIOLATIONS = [
"missing-whitespace-around-modulo-operator", "missing-whitespace-around-modulo-operator",
"missing-whitespace-around-operator", "missing-whitespace-around-operator",
"missing-whitespace-around-parameter-equals", "missing-whitespace-around-parameter-equals",
"module-import-not-at-top-of-file",
"multi-line-implicit-string-concatenation", "multi-line-implicit-string-concatenation",
"multiple-leading-hashes-for-block-comment", "multiple-leading-hashes-for-block-comment",
"multiple-spaces-after-comma", "multiple-spaces-after-comma",
@ -119,19 +116,46 @@ class CodeBlockError(Exception):
"""A code block parse error.""" """A code block parse error."""
def format_str( class InvalidInput(ValueError):
src: str, """Raised when ruff fails to parse file."""
black_mode: black.FileMode,
) -> tuple[str, Sequence[CodeBlockError]]:
"""Format a single docs file string.""" 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] = [] errors: list[CodeBlockError] = []
def _snipped_match(match: Match[str]) -> str: def _snipped_match(match: Match[str]) -> str:
code = textwrap.dedent(match["code"]) code = textwrap.dedent(match["code"])
try: try:
code = black.format_str(code, mode=black_mode) code = format_str(code)
except InvalidInput as e: except InvalidInput as e:
errors.append(CodeBlockError(e)) errors.append(CodeBlockError(e))
except NotImplementedError as e:
raise e
code = textwrap.indent(code, match["indent"]) code = textwrap.indent(code, match["indent"])
return f'{match["before"]}{code}{match["after"]}' return f'{match["before"]}{code}{match["after"]}'
@ -140,12 +164,7 @@ def format_str(
return src, errors return src, errors
def format_file( def format_file(file: Path, error_known: bool, args: argparse.Namespace) -> int:
file: Path,
black_mode: black.FileMode,
error_known: bool,
args: argparse.Namespace,
) -> int:
"""Check the formatting of a single docs file. """Check the formatting of a single docs file.
Returns the exit code for the script. Returns the exit code for the script.
@ -170,7 +189,7 @@ def format_file(
# Remove everything after the last example # Remove everything after the last example
contents = contents[: contents.rfind("```")] + "```" 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: if errors and not args.skip_errors and not error_known:
for error in errors: for error in errors:
@ -237,10 +256,6 @@ def main(argv: Sequence[str] | None = None) -> int:
print("Please generate rules first.") print("Please generate rules first.")
return 1 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 # Check known formatting violations and parse errors are sorted alphabetically and
# have no duplicates. This will reduce the diff when adding new violations # 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 violations = 0
errors = 0 errors = 0
print("Checking docs formatting...")
for file in [*static_docs, *generated_docs]: for file in [*static_docs, *generated_docs]:
rule_name = file.name.split(".")[0] rule_name = file.name.split(".")[0]
if rule_name in KNOWN_FORMATTING_VIOLATIONS: 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 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: if result == 1:
violations += 1 violations += 1
elif result == 2 and not error_known: 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: if violations > 0 or errors > 0:
return 1 return 1
print("All docs are formatted correctly.")
return 0 return 0