Skip to content

Commit 7b1bfe1

Browse files
authored
feat: list requires Python too (#669)
Signed-off-by: Henry Schreiner <[email protected]>
1 parent ff5c94e commit 7b1bfe1

File tree

5 files changed

+107
-111
lines changed

5 files changed

+107
-111
lines changed

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ messages_control.disable = [
148148
"invalid-name",
149149
"redefined-outer-name",
150150
"no-member", # better handled by mypy, etc.
151-
"duplicate-code", # Triggers on long match statements
152151
]
153152

154153

src/sp_repo_review/checks/pyproject.py

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,29 @@
88
from configparser import ConfigParser
99

1010

11+
def get_requires_python(
12+
pyproject: dict[str, Any], setupcfg: ConfigParser | None
13+
) -> str | None:
14+
match pyproject:
15+
case {"project": {"requires-python": str() as requires}}:
16+
return requires
17+
case {
18+
"tool": {
19+
"poetry": {
20+
"dependencies": {"python": str() as requires}
21+
| {"version": str() as requires}
22+
}
23+
}
24+
}:
25+
return requires
26+
27+
if setupcfg and (
28+
requires := setupcfg.get("options", "python_requires", fallback="")
29+
):
30+
return requires
31+
return None
32+
33+
1134
class PyProject:
1235
family = "pyproject"
1336

@@ -67,24 +90,9 @@ def check(pyproject: dict[str, Any], setupcfg: ConfigParser | None) -> bool | No
6790
you want to add a custom error message, add a build-type and/or runtime assert.
6891
"""
6992

70-
match pyproject:
71-
case {"project": {"requires-python": requires}}:
72-
return "~=" not in requires and "<" not in requires
73-
case {
74-
"tool": {
75-
"poetry": {
76-
"dependencies": {"python": requires} | {"version": requires}
77-
}
78-
}
79-
}:
80-
return (
81-
"^" not in requires and "~=" not in requires and "<" not in requires
82-
)
83-
84-
if setupcfg and (
85-
requires := setupcfg.get("options", "python_requires", fallback=None)
86-
):
87-
return "~=" not in requires and "<" not in requires
93+
requires = get_requires_python(pyproject, setupcfg)
94+
if requires is not None:
95+
return "^" not in requires and "~=" not in requires and "<" not in requires
8896

8997
return None
9098

src/sp_repo_review/checks/ruff.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@
1515
## R2xx: Ruff deprecations
1616

1717

18+
def get_rule_selection(ruff: dict[str, Any]) -> frozenset[str]:
19+
match ruff:
20+
case (
21+
{"lint": {"select": x} | {"extend-select": x}}
22+
| {"select": x}
23+
| {"extend-select": x}
24+
):
25+
return frozenset(x)
26+
case _:
27+
return frozenset()
28+
29+
1830
def merge(start: dict[str, Any], add: dict[str, Any]) -> dict[str, Any]:
1931
merged = start.copy()
2032
for key, value in add.items():
@@ -124,16 +136,8 @@ def check(cls: type[RF1xxMixin], ruff: dict[str, Any]) -> bool:
124136
]
125137
```
126138
"""
127-
128-
match ruff:
129-
case (
130-
{"lint": {"select": x} | {"extend-select": x}}
131-
| {"select": x}
132-
| {"extend-select": x}
133-
):
134-
return cls.code in x or "ALL" in x
135-
case _:
136-
return False
139+
selected_rules = get_rule_selection(ruff)
140+
return cls.code in selected_rules or "ALL" in selected_rules
137141

138142

139143
class RF101(RF1xx):

src/sp_repo_review/families.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
if TYPE_CHECKING:
77
from collections.abc import Generator
8+
from configparser import ConfigParser
89

10+
from .checks.pyproject import get_requires_python
11+
from .checks.ruff import get_rule_selection
912

1013
__all__ = ["Family", "get_families"]
1114

@@ -20,7 +23,9 @@ class Family(typing.TypedDict, total=False):
2023
description: str # Defaults to empty
2124

2225

23-
def general_description(pyproject: dict[str, Any]) -> Generator[str, None, None]:
26+
def general_description(
27+
pyproject: dict[str, Any], setupcfg: ConfigParser | None
28+
) -> Generator[str, None, None]:
2429
yield f"- Detected build backend: `{pyproject.get('build-system', {}).get('build-backend', 'MISSING')}`"
2530
match pyproject:
2631
case {"project": {"license": str() as license}}:
@@ -34,6 +39,10 @@ def general_description(pyproject: dict[str, Any]) -> Generator[str, None, None]
3439
if licenses:
3540
yield f"- Detected license(s): {', '.join(licenses)}"
3641

42+
requires = get_requires_python(pyproject, setupcfg)
43+
if requires is not None:
44+
yield f"- Python requires: `{requires}`"
45+
3746

3847
def ruff_description(ruff: dict[str, Any]) -> str:
3948
common = {
@@ -66,28 +75,26 @@ def ruff_description(ruff: dict[str, Any]) -> str:
6675
"UP",
6776
"YTT",
6877
}
69-
70-
match ruff:
71-
case (
72-
{"lint": {"select": x} | {"extend-select": x}}
73-
| {"select": x}
74-
| {"extend-select": x}
75-
):
76-
selected = set(x)
77-
known = common - selected
78-
if not known or "ALL" in selected:
79-
return "All mentioned rules selected"
80-
rulelist = ", ".join(f'"{r}"' for r in known)
81-
return f"Rules mentioned in guide but not here: `{rulelist}`"
78+
selected = get_rule_selection(ruff)
79+
if selected:
80+
known = common - selected
81+
if not known or "ALL" in selected:
82+
return "All mentioned rules selected"
83+
rulelist = ", ".join(f'"{r}"' for r in known)
84+
return f"Rules mentioned in guide but not here: `{rulelist}`"
8285
return ""
8386

8487

85-
def get_families(pyproject: dict[str, Any], ruff: dict[str, Any]) -> dict[str, Family]:
88+
def get_families(
89+
pyproject: dict[str, Any],
90+
ruff: dict[str, Any],
91+
setupcfg: ConfigParser | None = None,
92+
) -> dict[str, Family]:
8693
return {
8794
"general": Family(
8895
name="General",
8996
order=-3,
90-
description="\n".join(general_description(pyproject)),
97+
description="\n".join(general_description(pyproject, setupcfg)),
9198
),
9299
"pyproject": Family(
93100
name="PyProject",

tests/test_families.py

Lines changed: 44 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -47,42 +47,51 @@ def test_classic_license():
4747
)
4848

4949

50+
def test_python_requires():
51+
pyproject = {
52+
"project": {"requires-python": ">=3.13"},
53+
}
54+
families = get_families(pyproject, {})
55+
assert families["general"].get("description") == (
56+
"- Detected build backend: `MISSING`\n- Python requires: `>=3.13`"
57+
)
58+
59+
60+
ALL_RULES = [
61+
"ARG",
62+
"B",
63+
"C4",
64+
"DTZ",
65+
"EM",
66+
"EXE",
67+
"FA",
68+
"FURB",
69+
"G",
70+
"I",
71+
"ICN",
72+
"NPY",
73+
"PD",
74+
"PERF",
75+
"PGH",
76+
"PIE",
77+
"PL",
78+
"PT",
79+
"PTH",
80+
"PYI",
81+
"RET",
82+
"RUF",
83+
"SIM",
84+
"SLOT",
85+
"T20",
86+
"TC",
87+
"UP",
88+
"YTT",
89+
]
90+
91+
5092
def test_ruff_all_rules_selected():
5193
"""Test when all recommended rules are selected."""
52-
ruff = {
53-
"lint": {
54-
"select": [
55-
"ARG",
56-
"B",
57-
"C4",
58-
"DTZ",
59-
"EM",
60-
"EXE",
61-
"FA",
62-
"FURB",
63-
"G",
64-
"I",
65-
"ICN",
66-
"NPY",
67-
"PD",
68-
"PERF",
69-
"PGH",
70-
"PIE",
71-
"PL",
72-
"PT",
73-
"PTH",
74-
"PYI",
75-
"RET",
76-
"RUF",
77-
"SIM",
78-
"SLOT",
79-
"T20",
80-
"TC",
81-
"UP",
82-
"YTT",
83-
]
84-
}
85-
}
94+
ruff = {"lint": {"select": ALL_RULES}}
8695
families = get_families({}, ruff)
8796
assert families["ruff"].get("description") == "All mentioned rules selected"
8897

@@ -116,38 +125,7 @@ def test_ruff_extend_select():
116125

117126
def test_ruff_root_level_select():
118127
"""Test with select at root level (not under lint)."""
119-
ruff = {
120-
"select": [
121-
"ARG",
122-
"B",
123-
"C4",
124-
"DTZ",
125-
"EM",
126-
"EXE",
127-
"FA",
128-
"FURB",
129-
"G",
130-
"I",
131-
"ICN",
132-
"NPY",
133-
"PD",
134-
"PERF",
135-
"PGH",
136-
"PIE",
137-
"PL",
138-
"PT",
139-
"PTH",
140-
"PYI",
141-
"RET",
142-
"RUF",
143-
"SIM",
144-
"SLOT",
145-
"T20",
146-
"TC",
147-
"UP",
148-
"YTT",
149-
]
150-
}
128+
ruff = {"select": ALL_RULES}
151129
families = get_families({}, ruff)
152130
assert families["ruff"].get("description") == "All mentioned rules selected"
153131

0 commit comments

Comments
 (0)