Skip to content

Commit ff5c94e

Browse files
henryiiiCopilot
andauthored
feat: note rules not followed in ruff description (#667)
* feat: note rules not followed in ruff description Signed-off-by: Henry Schreiner <[email protected]> * test: add tests for ruff family description feature (#668) * Initial plan * test: add tests for ruff family description Co-authored-by: henryiii <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: henryiii <[email protected]> * chore: fixup some warnings Signed-off-by: Henry Schreiner <[email protected]> --------- Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: henryiii <[email protected]>
1 parent 7a17e5a commit ff5c94e

File tree

3 files changed

+193
-17
lines changed

3 files changed

+193
-17
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ 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
151152
]
152153

153154

src/sp_repo_review/families.py

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
from __future__ import annotations
22

33
import typing
4-
from typing import Any
4+
from typing import TYPE_CHECKING, Any
5+
6+
if TYPE_CHECKING:
7+
from collections.abc import Generator
8+
59

610
__all__ = ["Family", "get_families"]
711

@@ -16,23 +20,74 @@ class Family(typing.TypedDict, total=False):
1620
description: str # Defaults to empty
1721

1822

19-
def get_families(pyproject: dict[str, Any]) -> dict[str, Family]:
20-
pyproject_description = f"- Detected build backend: `{pyproject.get('build-system', {}).get('build-backend', 'MISSING')}`"
21-
if isinstance(license := pyproject.get("project", {}).get("license", {}), str):
22-
pyproject_description += f"\n- SPDX license expression: `{license}`"
23-
elif classifiers := pyproject.get("project", {}).get("classifiers", []):
24-
licenses = [
25-
c.removeprefix("License :: ").removeprefix("OSI Approved :: ")
26-
for c in classifiers
27-
if c.startswith("License :: ")
28-
]
29-
if licenses:
30-
pyproject_description += f"\n- Detected license(s): {', '.join(licenses)}"
23+
def general_description(pyproject: dict[str, Any]) -> Generator[str, None, None]:
24+
yield f"- Detected build backend: `{pyproject.get('build-system', {}).get('build-backend', 'MISSING')}`"
25+
match pyproject:
26+
case {"project": {"license": str() as license}}:
27+
yield f"- SPDX license expression: `{license}`"
28+
case {"project": {"classifiers": classifiers}}:
29+
licenses = [
30+
c.removeprefix("License :: ").removeprefix("OSI Approved :: ")
31+
for c in classifiers
32+
if c.startswith("License :: ")
33+
]
34+
if licenses:
35+
yield f"- Detected license(s): {', '.join(licenses)}"
36+
37+
38+
def ruff_description(ruff: dict[str, Any]) -> str:
39+
common = {
40+
"ARG",
41+
"B",
42+
"C4",
43+
"DTZ",
44+
"EM",
45+
"EXE",
46+
"FA",
47+
"FURB",
48+
"G",
49+
"I",
50+
"ICN",
51+
"NPY",
52+
"PD",
53+
"PERF",
54+
"PGH",
55+
"PIE",
56+
"PL",
57+
"PT",
58+
"PTH",
59+
"PYI",
60+
"RET",
61+
"RUF",
62+
"SIM",
63+
"SLOT",
64+
"T20",
65+
"TC",
66+
"UP",
67+
"YTT",
68+
}
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}`"
82+
return ""
83+
84+
85+
def get_families(pyproject: dict[str, Any], ruff: dict[str, Any]) -> dict[str, Family]:
3186
return {
3287
"general": Family(
3388
name="General",
3489
order=-3,
35-
description=pyproject_description,
90+
description="\n".join(general_description(pyproject)),
3691
),
3792
"pyproject": Family(
3893
name="PyProject",
@@ -49,6 +104,7 @@ def get_families(pyproject: dict[str, Any]) -> dict[str, Family]:
49104
),
50105
"ruff": Family(
51106
name="Ruff",
107+
description=ruff_description(ruff),
52108
),
53109
"docs": Family(
54110
name="Documentation",

tests/test_families.py

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def test_backend():
88
"build-backend": "setuptools.build_meta",
99
},
1010
}
11-
families = get_families(pyproject)
11+
families = get_families(pyproject, {})
1212
assert families["general"].get("description") == (
1313
"- Detected build backend: `setuptools.build_meta`"
1414
)
@@ -24,7 +24,7 @@ def test_spdx_license():
2424
],
2525
},
2626
}
27-
families = get_families(pyproject)
27+
families = get_families(pyproject, {})
2828
assert families["general"].get("description") == (
2929
"- Detected build backend: `MISSING`\n- SPDX license expression: `MIT`"
3030
)
@@ -40,8 +40,127 @@ def test_classic_license():
4040
],
4141
},
4242
}
43-
families = get_families(pyproject)
43+
families = get_families(pyproject, {})
4444
assert families["general"].get("description") == (
4545
"- Detected build backend: `MISSING`\n"
4646
"- Detected license(s): MIT License, BSD License"
4747
)
48+
49+
50+
def test_ruff_all_rules_selected():
51+
"""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+
}
86+
families = get_families({}, ruff)
87+
assert families["ruff"].get("description") == "All mentioned rules selected"
88+
89+
90+
def test_ruff_all_keyword():
91+
"""Test when 'ALL' keyword is used."""
92+
ruff = {"lint": {"select": ["ALL"]}}
93+
families = get_families({}, ruff)
94+
assert families["ruff"].get("description") == "All mentioned rules selected"
95+
96+
97+
def test_ruff_missing_rules():
98+
"""Test when some recommended rules are missing."""
99+
ruff = {"lint": {"select": ["B", "I", "UP"]}}
100+
families = get_families({}, ruff)
101+
description = families["ruff"].get("description", "")
102+
assert description.startswith("Rules mentioned in guide but not here:")
103+
# Check that some missing rules are mentioned
104+
assert "ARG" in description or "C4" in description or "DTZ" in description
105+
106+
107+
def test_ruff_extend_select():
108+
"""Test with extend-select instead of select."""
109+
ruff = {"lint": {"extend-select": ["B", "I", "UP", "RUF"]}}
110+
families = get_families({}, ruff)
111+
description = families["ruff"].get("description", "")
112+
assert description.startswith("Rules mentioned in guide but not here:")
113+
# Verify that some rules are listed as missing
114+
assert "ARG" in description or "C4" in description
115+
116+
117+
def test_ruff_root_level_select():
118+
"""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+
}
151+
families = get_families({}, ruff)
152+
assert families["ruff"].get("description") == "All mentioned rules selected"
153+
154+
155+
def test_ruff_empty_config():
156+
"""Test with empty ruff config."""
157+
ruff: dict[str, object] = {}
158+
families = get_families({}, ruff)
159+
assert families["ruff"].get("description") == ""
160+
161+
162+
def test_ruff_no_select():
163+
"""Test ruff config without select or extend-select."""
164+
ruff = {"lint": {"ignore": ["E501"]}}
165+
families = get_families({}, ruff)
166+
assert families["ruff"].get("description") == ""

0 commit comments

Comments
 (0)