Skip to content

Instantly share code, notes, and snippets.

@pcorpet
Created September 19, 2024 07:47
Show Gist options
  • Save pcorpet/e776a8e794264b818c9cc6d06c11ef15 to your computer and use it in GitHub Desktop.
Save pcorpet/e776a8e794264b818c9cc6d06c11ef15 to your computer and use it in GitHub Desktop.
Script to help migrate Pylint config to Ruff.
#!/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