pypa-hatch/scripts/update_ruff.py

173 lines
5.8 KiB
Python

from __future__ import annotations
import json
import re
import subprocess
import sys
import typing
from importlib.metadata import version
from utils import ROOT
if typing.TYPE_CHECKING:
from pathlib import Path
# fmt: off
UNSELECTED_RULE_PATTERNS: list[str] = [
# Allow non-abstract empty methods in abstract base classes
'B027',
# Allow boolean positional values in function calls, like `dict.get(... True)`
'FBT003',
# Ignore complexity
'C901', 'PLR0904', 'PLR0911', 'PLR0912', 'PLR0913', 'PLR0914', 'PLR0915', 'PLR0916', 'PLR0917', 'PLR1702',
# These are dependent on projects themselves
'AIR\\d+', 'CPY\\d+', 'D\\d+', 'DJ\\d+', 'NPY\\d+', 'PD\\d+',
# Many projects either don't have type annotations or it would take much effort to satisfy this
'ANN\\d+',
# Don't be too strict about TODOs as not everyone uses them the same way
'FIX\\d+', 'TD001', 'TD002', 'TD003',
# There are valid reasons to not use pathlib such as performance and import cost
'PTH\\d+', 'FURB101', 'FURB103',
# Conflicts with type checking
'RET501', 'RET502',
# Under review https://github.com/astral-sh/ruff/issues/8796
'PT004', 'PT005',
# Buggy https://github.com/astral-sh/ruff/issues/4845
'ERA001',
# Business logic relying on other programs has no choice but to use subprocess
'S404',
# Too prone to false positives and might be removed https://github.com/astral-sh/ruff/issues/4045
'S603',
# Too prone to false positives https://github.com/astral-sh/ruff/issues/8761
'SIM401',
# Allow for easy ignores
'PGH003', 'PGH004',
# This is required sometimes, and doesn't matter on Python 3.11+
'PERF203',
# Potentially unnecessary on Python 3.12+
'FURB140',
# Conflicts with formatter, see:
# https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
'COM812', 'COM819', 'D206', 'D300', 'E111', 'E114', 'E117', 'E301', 'E302', 'E303', 'E304', 'E305', 'E306', "E501", 'ISC001', 'ISC002', 'Q000', 'Q001', 'Q002', 'Q003', 'Q004', 'W191',
# Conflicts with context formatting in dependencies
'RUF200',
# Currently broken
]
PER_FILE_IGNORED_RULES: dict[str, list[str]] = {
'**/scripts/*': [
# Implicit namespace packages
'INP001',
# Print statements
'T201',
],
'**/tests/**/*': [
# Empty string comparisons
'PLC1901',
# Magic values
'PLR2004',
# Methods that don't use `self`
'PLR6301',
# Potential security issues like assert statements and hardcoded passwords
'S',
# Relative imports
'TID252',
],
}
# fmt: on
def get_lines_until(file_path: Path, marker: str) -> list[str]:
lines = file_path.read_text(encoding='utf-8').splitlines()
for i, line in enumerate(lines):
if line.startswith(marker):
block_start = i
break
else:
message = f'Could not find {marker}: {file_path.relative_to(ROOT)}'
raise ValueError(message)
del lines[block_start:]
return lines
def main():
process = subprocess.run( # noqa: PLW1510
[sys.executable, '-m', 'ruff', 'rule', '--all', '--output-format', 'json'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
encoding='utf-8',
cwd=str(ROOT),
)
if process.returncode:
raise OSError(process.stdout)
data_file = ROOT / 'src' / 'hatch' / 'cli' / 'fmt' / 'core.py'
lines = get_lines_until(data_file, 'STABLE_RULES')
ignored_pattern = re.compile(f'^({"|".join(UNSELECTED_RULE_PATTERNS)})$')
# https://github.com/astral-sh/ruff/issues/9891#issuecomment-1951403651
removed_pattern = re.compile(r'^\s*#+\s+(removed|removal)', flags=re.IGNORECASE | re.MULTILINE)
stable_rules: set[str] = set()
preview_rules: set[str] = set()
unselected_rules: set[str] = set()
for rule in json.loads(process.stdout):
code = rule['code']
if ignored_pattern.match(code) or removed_pattern.search(rule['explanation']):
unselected_rules.add(code)
continue
if rule['preview']:
preview_rules.add(code)
else:
stable_rules.add(code)
lines.append('STABLE_RULES: tuple[str, ...] = (')
lines.extend(f' {rule!r},' for rule in sorted(stable_rules))
lines.append(')')
lines.append('PREVIEW_RULES: tuple[str, ...] = (')
lines.extend(f' {rule!r},' for rule in sorted(preview_rules))
lines.append(')')
lines.append('PER_FILE_IGNORED_RULES: dict[str, list[str]] = {')
for ignored_glob, ignored_rules in sorted(PER_FILE_IGNORED_RULES.items()):
lines.append(f' {ignored_glob!r}: [')
lines.extend(f' {rule!r},' for rule in sorted(ignored_rules))
lines.append(' ],')
lines.append('}')
lines.append('')
data_file.write_text('\n'.join(lines), encoding='utf-8')
version_file = ROOT / 'src' / 'hatch' / 'env' / 'internal' / 'static_analysis.py'
latest_version = version('ruff')
version_file.write_text(
re.sub(
r'^(RUFF_DEFAULT_VERSION.+=.+\').+?(\')$',
rf'\g<1>{latest_version}\g<2>',
version_file.read_text(encoding='utf-8'),
count=1,
flags=re.MULTILINE,
),
encoding='utf-8',
)
data_file = ROOT / 'docs' / '.hooks' / 'render_ruff_defaults.py'
lines = get_lines_until(data_file, 'UNSELECTED_RULES')
lines.append('UNSELECTED_RULES: tuple[str, ...] = (')
lines.extend(f' {rule!r},' for rule in sorted(unselected_rules))
lines.append(')')
lines.append('')
data_file.write_text('\n'.join(lines), encoding='utf-8')
print(f'Stable rules: {len(stable_rules)}')
print(f'Preview rules: {len(preview_rules)}')
print(f'Unselected rules: {len(unselected_rules)}')
if __name__ == '__main__':
main()