Created
September 19, 2024 07:47
-
-
Save pcorpet/e776a8e794264b818c9cc6d06c11ef15 to your computer and use it in GitHub Desktop.
Script to help migrate Pylint config to Ruff.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# Script to help migrate Pylint config to Ruff. | |
# | |
# It lists enabled Pylint rules, Pylint rules that are enabled but already covered by Ruff rules, | |
# and the Ruff rules not enabled yet that match enabled Pylint rules. | |
# | |
# Largely based on https://github.com/astral-sh/ruff/issues/970 | |
import re | |
import subprocess | |
from collections.abc import Mapping, Sequence | |
_RUFF_PYLINT_ISSUE_URL = "https://api.github.com/repos/astral-sh/ruff/issues/970" | |
def _get_all_available_pylint_rules() -> frozenset[str]: | |
result = subprocess.run( | |
["pylint", "--list-msgs"], | |
capture_output=True, | |
text=True, | |
check=False, | |
) | |
# Example output: | |
# :non-ascii-file-name (W2402): *%s name "%s" contains a non-ASCII character.* | |
# Under python 3.5, PEP 3131 allows non-ascii identifiers, but not non-ascii | |
# file names.Since Python 3.5, even though Python supports UTF-8 files, some | |
# editors or tools don't. | |
all_rules = frozenset( | |
{ | |
line.split()[0][1:] | |
for line in result.stdout.splitlines() | |
if line.startswith(":") | |
}, | |
) | |
return all_rules | |
def _get_all_disabled_pylint_rules() -> frozenset[str]: | |
result = subprocess.run( | |
["pylint", "--generate-rcfile"], | |
capture_output=True, | |
text=True, | |
check=False, | |
) | |
disabled_match = re.search( | |
r"(?:\n|^)disable\s*=\s*([^\n#]+(?:\n\s+[^\n#]+)*)", | |
result.stdout, | |
) | |
if not disabled_match: | |
return frozenset() | |
disabled_rules = frozenset( | |
# Extract individual rule codes | |
rule.strip() | |
for rule in disabled_match.group(1).split(",") | |
) | |
return disabled_rules | |
def _get_enabled_pylint_rules() -> frozenset[str]: | |
all_rules = _get_all_available_pylint_rules() | |
disabled_rules = _get_all_disabled_pylint_rules() | |
enabled_rules = all_rules - disabled_rules | |
return enabled_rules | |
def _get_pylint_to_ruff_rules_mapping() -> Mapping[str, tuple[str, Sequence[str]]]: | |
"""For each pylint rule handled by Ruff, get its pylint code and the code of the corresponding Ruff rules.""" | |
result = subprocess.run( | |
f"curl {_RUFF_PYLINT_ISSUE_URL} | jq -r .body | grep -F '[x]' | grep -v '~'", | |
capture_output=True, | |
text=True, | |
check=False, | |
shell=True, | |
) | |
ruff_rules_mapping = { | |
resolved_match.group(1): ( | |
resolved_match.group(2), | |
tuple( | |
r.strip() for r in resolved_match.group(3).replace("`", "").split(",") | |
), | |
) | |
for line in result.stdout.splitlines() | |
if (resolved_match := re.search(r"`([^`]+)` / `([^`]+)` \(`([^)]+)`\)", line)) | |
} | |
return ruff_rules_mapping | |
def _get_enabled_ruff_rules() -> frozenset[str]: | |
result = subprocess.run( | |
["ruff", "check", "--show-settings"], | |
capture_output=True, | |
text=True, | |
check=False, | |
) | |
enabled_match = re.search( | |
r"(?:\n|^)linter.rules.enabled\s*=\s*\[\s*([^\n#]+(?:\n\s+[^\n#]+)*)\s*\]", | |
result.stdout, | |
) | |
if not enabled_match: | |
return frozenset() | |
enabled_rules = frozenset( | |
# Extract individual rule codes | |
rule.split("(")[1].split(")")[0].strip() | |
for rule in enabled_match.group(1).split(",") | |
if "(" in rule and ")" in rule | |
) | |
return enabled_rules | |
def _get_pylint_useless_rules() -> frozenset[str]: | |
"""Pylint rules that are already covered by our enabled Ruff rules.""" | |
pylint_to_ruff_rules = _get_pylint_to_ruff_rules_mapping() | |
enabled_ruff_rules = _get_enabled_ruff_rules() | |
pylint_useless_rules = frozenset( | |
ruff_rule[0] | |
for pylint_rule in enabled_pylint_rules | |
if (ruff_rule := pylint_to_ruff_rules.get(pylint_rule)) | |
and set(ruff_rule[1]) <= enabled_ruff_rules | |
) | |
return pylint_useless_rules | |
def _get_ruff_rules_to_enable() -> frozenset[str]: | |
"""Ruff rules that correspond to enabled Pylint rules.""" | |
pylint_to_ruff_rules = _get_pylint_to_ruff_rules_mapping() | |
enabled_pylint_rules = _get_enabled_pylint_rules() | |
enabled_ruff_rules = _get_enabled_ruff_rules() | |
ruff_rules_covering_for_enabled_pylint_rules = frozenset( | |
ruff_rule | |
for pylint_rule in enabled_pylint_rules | |
if (ruff_rule_mapping := pylint_to_ruff_rules.get(pylint_rule)) | |
for ruff_rule in ruff_rule_mapping[1] | |
) | |
return ruff_rules_covering_for_enabled_pylint_rules - enabled_ruff_rules | |
enabled_pylint_rules = _get_enabled_pylint_rules() | |
print("Enabled rules:", len(enabled_pylint_rules)) | |
for rule in sorted(enabled_pylint_rules): | |
print(rule) | |
pylint_useless_rules = _get_pylint_useless_rules() | |
if pylint_useless_rules: | |
print("\nPylint rules already covered by our enabled Ruff rules:") | |
print(",".join(sorted(pylint_useless_rules))) | |
else: | |
print("\nAll Pylint rules covered by Ruff have been disabled.") | |
ruff_rules_to_enable = _get_ruff_rules_to_enable() | |
if ruff_rules_to_enable: | |
print("\nRuff rules to enable:") | |
for rule in sorted(ruff_rules_to_enable): | |
print(rule) | |
else: | |
print("\nNo Ruff rules to enable.") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment