Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
]


Expand Down
44 changes: 26 additions & 18 deletions src/sp_repo_review/checks/pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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

Expand Down
24 changes: 14 additions & 10 deletions src/sp_repo_review/checks/ruff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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):
Expand Down
39 changes: 23 additions & 16 deletions src/sp_repo_review/families.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand All @@ -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}}:
Expand All @@ -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 = {
Expand Down Expand Up @@ -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",
Expand Down
110 changes: 44 additions & 66 deletions tests/test_families.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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"

Expand Down