diff --git a/pyproject.toml b/pyproject.toml index 7bcea63e..f6af1144 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -148,7 +148,6 @@ messages_control.disable = [ "invalid-name", "redefined-outer-name", "no-member", # better handled by mypy, etc. - "duplicate-code", # Triggers on long match statements ] diff --git a/src/sp_repo_review/checks/pyproject.py b/src/sp_repo_review/checks/pyproject.py index e56da804..7121018b 100644 --- a/src/sp_repo_review/checks/pyproject.py +++ b/src/sp_repo_review/checks/pyproject.py @@ -8,6 +8,29 @@ from configparser import ConfigParser +def get_requires_python( + pyproject: dict[str, Any], setupcfg: ConfigParser | None +) -> str | None: + match pyproject: + case {"project": {"requires-python": str() as requires}}: + return requires + case { + "tool": { + "poetry": { + "dependencies": {"python": str() as requires} + | {"version": str() as requires} + } + } + }: + return requires + + if setupcfg and ( + requires := setupcfg.get("options", "python_requires", fallback="") + ): + return requires + return None + + class PyProject: family = "pyproject" @@ -67,24 +90,9 @@ def check(pyproject: dict[str, Any], setupcfg: ConfigParser | None) -> bool | No you want to add a custom error message, add a build-type and/or runtime assert. """ - match pyproject: - case {"project": {"requires-python": requires}}: - return "~=" not in requires and "<" not in requires - case { - "tool": { - "poetry": { - "dependencies": {"python": requires} | {"version": requires} - } - } - }: - return ( - "^" not in requires and "~=" not in requires and "<" not in requires - ) - - if setupcfg and ( - requires := setupcfg.get("options", "python_requires", fallback=None) - ): - return "~=" not in requires and "<" not in requires + requires = get_requires_python(pyproject, setupcfg) + if requires is not None: + return "^" not in requires and "~=" not in requires and "<" not in requires return None diff --git a/src/sp_repo_review/checks/ruff.py b/src/sp_repo_review/checks/ruff.py index b10f506e..a1d7c572 100644 --- a/src/sp_repo_review/checks/ruff.py +++ b/src/sp_repo_review/checks/ruff.py @@ -15,6 +15,18 @@ ## R2xx: Ruff deprecations +def get_rule_selection(ruff: dict[str, Any]) -> frozenset[str]: + match ruff: + case ( + {"lint": {"select": x} | {"extend-select": x}} + | {"select": x} + | {"extend-select": x} + ): + return frozenset(x) + case _: + return frozenset() + + def merge(start: dict[str, Any], add: dict[str, Any]) -> dict[str, Any]: merged = start.copy() for key, value in add.items(): @@ -124,16 +136,8 @@ def check(cls: type[RF1xxMixin], ruff: dict[str, Any]) -> bool: ] ``` """ - - match ruff: - case ( - {"lint": {"select": x} | {"extend-select": x}} - | {"select": x} - | {"extend-select": x} - ): - return cls.code in x or "ALL" in x - case _: - return False + selected_rules = get_rule_selection(ruff) + return cls.code in selected_rules or "ALL" in selected_rules class RF101(RF1xx): diff --git a/src/sp_repo_review/families.py b/src/sp_repo_review/families.py index d2909214..d1d0fbed 100644 --- a/src/sp_repo_review/families.py +++ b/src/sp_repo_review/families.py @@ -5,7 +5,10 @@ if TYPE_CHECKING: from collections.abc import Generator + from configparser import ConfigParser +from .checks.pyproject import get_requires_python +from .checks.ruff import get_rule_selection __all__ = ["Family", "get_families"] @@ -20,7 +23,9 @@ class Family(typing.TypedDict, total=False): description: str # Defaults to empty -def general_description(pyproject: dict[str, Any]) -> Generator[str, None, None]: +def general_description( + pyproject: dict[str, Any], setupcfg: ConfigParser | None +) -> Generator[str, None, None]: yield f"- Detected build backend: `{pyproject.get('build-system', {}).get('build-backend', 'MISSING')}`" match pyproject: case {"project": {"license": str() as license}}: @@ -34,6 +39,10 @@ def general_description(pyproject: dict[str, Any]) -> Generator[str, None, None] if licenses: yield f"- Detected license(s): {', '.join(licenses)}" + requires = get_requires_python(pyproject, setupcfg) + if requires is not None: + yield f"- Python requires: `{requires}`" + def ruff_description(ruff: dict[str, Any]) -> str: common = { @@ -66,28 +75,26 @@ def ruff_description(ruff: dict[str, Any]) -> str: "UP", "YTT", } - - match ruff: - case ( - {"lint": {"select": x} | {"extend-select": x}} - | {"select": x} - | {"extend-select": x} - ): - selected = set(x) - known = common - selected - if not known or "ALL" in selected: - return "All mentioned rules selected" - rulelist = ", ".join(f'"{r}"' for r in known) - return f"Rules mentioned in guide but not here: `{rulelist}`" + selected = get_rule_selection(ruff) + if selected: + known = common - selected + if not known or "ALL" in selected: + return "All mentioned rules selected" + rulelist = ", ".join(f'"{r}"' for r in known) + return f"Rules mentioned in guide but not here: `{rulelist}`" return "" -def get_families(pyproject: dict[str, Any], ruff: dict[str, Any]) -> dict[str, Family]: +def get_families( + pyproject: dict[str, Any], + ruff: dict[str, Any], + setupcfg: ConfigParser | None = None, +) -> dict[str, Family]: return { "general": Family( name="General", order=-3, - description="\n".join(general_description(pyproject)), + description="\n".join(general_description(pyproject, setupcfg)), ), "pyproject": Family( name="PyProject", diff --git a/tests/test_families.py b/tests/test_families.py index f73080f4..af450c47 100644 --- a/tests/test_families.py +++ b/tests/test_families.py @@ -47,42 +47,51 @@ def test_classic_license(): ) +def test_python_requires(): + pyproject = { + "project": {"requires-python": ">=3.13"}, + } + families = get_families(pyproject, {}) + assert families["general"].get("description") == ( + "- Detected build backend: `MISSING`\n- Python requires: `>=3.13`" + ) + + +ALL_RULES = [ + "ARG", + "B", + "C4", + "DTZ", + "EM", + "EXE", + "FA", + "FURB", + "G", + "I", + "ICN", + "NPY", + "PD", + "PERF", + "PGH", + "PIE", + "PL", + "PT", + "PTH", + "PYI", + "RET", + "RUF", + "SIM", + "SLOT", + "T20", + "TC", + "UP", + "YTT", +] + + def test_ruff_all_rules_selected(): """Test when all recommended rules are selected.""" - ruff = { - "lint": { - "select": [ - "ARG", - "B", - "C4", - "DTZ", - "EM", - "EXE", - "FA", - "FURB", - "G", - "I", - "ICN", - "NPY", - "PD", - "PERF", - "PGH", - "PIE", - "PL", - "PT", - "PTH", - "PYI", - "RET", - "RUF", - "SIM", - "SLOT", - "T20", - "TC", - "UP", - "YTT", - ] - } - } + ruff = {"lint": {"select": ALL_RULES}} families = get_families({}, ruff) assert families["ruff"].get("description") == "All mentioned rules selected" @@ -116,38 +125,7 @@ def test_ruff_extend_select(): def test_ruff_root_level_select(): """Test with select at root level (not under lint).""" - ruff = { - "select": [ - "ARG", - "B", - "C4", - "DTZ", - "EM", - "EXE", - "FA", - "FURB", - "G", - "I", - "ICN", - "NPY", - "PD", - "PERF", - "PGH", - "PIE", - "PL", - "PT", - "PTH", - "PYI", - "RET", - "RUF", - "SIM", - "SLOT", - "T20", - "TC", - "UP", - "YTT", - ] - } + ruff = {"select": ALL_RULES} families = get_families({}, ruff) assert families["ruff"].get("description") == "All mentioned rules selected"