Skip to content

Commit 76af623

Browse files
authored
feat: support reasons for ignores (#258)
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
1 parent 60f583a commit 76af623

File tree

9 files changed

+133
-23
lines changed

9 files changed

+133
-23
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ select = ["A", "B", "C100"]
6464
ignore = ["A100"]
6565
```
6666

67+
The ignore list can also be a table, with reasons for values.
68+
6769
If `--select` or `--ignore` are given on the command line, they will override
6870
the `pyproject.toml` config. You can use `--extend-select` and `--extend-ignore`
6971
on the command line to extend the `pyproject.toml` config. These CLI options

docs/intro.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,26 @@ You can explicitly list checks to select or skip in your `pyproject.toml`:
5151

5252
```toml
5353
[tool.repo-review]
54-
select = ["..."]
55-
ignore = ["..."]
54+
select = ["A", "B", "C100"]
55+
ignore = ["A100"]
5656
```
5757

58+
You can list the letter prefix or the exact check name. The ignore list can also
59+
be a table, with reasons for values. These will be shown explicitly in the report if
60+
a reason is given.
61+
62+
```toml
63+
[tool.repo-review.ignore]
64+
A = "Skipping this whole family"
65+
B101 = "Skipping this specific check"
66+
C101 = "" # Hidden from report, like a normal ignore
67+
```
68+
69+
If `--select` or `--ignore` are given on the command line, they will override
70+
the `pyproject.toml` config. You can use `--extend-select` and `--extend-ignore`
71+
on the command line to extend the `pyproject.toml` config. These CLI options
72+
are comma separated.
73+
5874
## Pre-commit
5975

6076
You can also use this from pre-commit:

src/repo_review/__main__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ def rich_printer(
163163
msg.append(rich.text.Text.from_markup(description, style=style))
164164
if result.result is None:
165165
msg.append(" [skipped]", style="yellow bold")
166+
if result.skip_reason:
167+
sr_style = "yellow"
168+
msg.append(" (", style=sr_style)
169+
msg.append(
170+
rich.text.Text.from_markup(result.skip_reason, style=sr_style)
171+
)
172+
msg.append(")", style=sr_style)
166173
tree.add(msg)
167174
elif result.result:
168175
msg.append(rich.text.Text.from_markup(" :white_check_mark:"))

src/repo_review/checks.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from .fixtures import apply_fixtures
88

9-
__all__ = ["Check", "collect_checks", "get_check_url", "is_allowed"]
9+
__all__ = ["Check", "collect_checks", "get_check_url", "is_allowed", "name_matches"]
1010

1111

1212
def __dir__() -> list[str]:
@@ -70,6 +70,25 @@ def collect_checks(fixtures: Mapping[str, Any]) -> dict[str, Check]:
7070
}
7171

7272

73+
def name_matches(name: str, selectors: Set[str]) -> str:
74+
"""
75+
Checks if the name is contained in the matchers. The selectors can be the
76+
exact name or just the non-number prefix. Returns the selector that matched,
77+
or an empty string if no match.
78+
79+
:param name: The name to check.
80+
:param expr: The expression to check against.
81+
82+
:return: The matched selector if the name matches a selector, or an empty string if no match.
83+
"""
84+
if name in selectors:
85+
return name
86+
short_name = name.rstrip("0123456789")
87+
if short_name in selectors:
88+
return short_name
89+
return ""
90+
91+
7392
def is_allowed(select: Set[str], ignore: Set[str], name: str) -> bool:
7493
"""
7594
Skips the check if the name is in the ignore list or if the name without the
@@ -82,15 +101,10 @@ def is_allowed(select: Set[str], ignore: Set[str], name: str) -> bool:
82101
83102
:return: True if this check is allowed, False otherwise.
84103
"""
85-
if (
86-
select
87-
and name not in select
88-
and name.rstrip("0123456789") not in select
89-
and "*" not in select
90-
):
104+
if select and not name_matches(name, select) and "*" not in select:
91105
return False
92106

93-
return name not in ignore and name.rstrip("0123456789") not in ignore
107+
return not name_matches(name, ignore)
94108

95109

96110
def get_check_url(name: str, check: Check) -> str:

src/repo_review/html.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def to_html(
6161
else "red"
6262
)
6363
icon = (
64-
"&#9888;&#65039;"
64+
("&#128311;" if result.skip_reason else "&#9888;&#65039;")
6565
if result.result is None
6666
else "&#9989;"
6767
if result.result
@@ -79,6 +79,11 @@ def to_html(
7979
if result.url
8080
else result.description
8181
)
82+
if result.skip_reason:
83+
description += (
84+
f'<br/><span style="color:DarkKhaki;"><b>Skipped:</b> '
85+
f"<em>{md.render(result.skip_reason)}</em></span>"
86+
)
8287
print(f'<tr style="color: {color};">')
8388
print(f'<td><span role="img" aria-label="{result_txt}">{icon}</span></td>')
8489
print(f'<td nowrap="nowrap">{result.name}</td>')

src/repo_review/processor.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
collect_checks,
1818
get_check_url,
1919
is_allowed,
20+
name_matches,
2021
process_result_bool,
2122
)
2223
from .families import Family, collect_families
@@ -63,6 +64,7 @@ class ResultDict(typing.TypedDict):
6364
result: bool | None #: The result, None means skip
6465
err_msg: str #: The error message if the result is false, in markdown format
6566
url: str #: An optional URL (empty string if missing)
67+
skip_reason: str #: The reason for the skip, if given (empty string if not)
6668

6769

6870
@dataclasses.dataclass(frozen=True, kw_only=True)
@@ -75,6 +77,7 @@ class Result:
7577
name: str #: The name of the check
7678
description: str #: The short description of what the check looks for
7779
result: bool | None #: The result, None means skip
80+
skip_reason: str = "" #: The reason for the skip, if given
7881
err_msg: str = "" #: The error message if the result is false, in markdown format
7982
url: str = "" #: An optional URL (empty string if missing)
8083

@@ -201,12 +204,12 @@ def process(
201204

202205
# Collect our own config
203206
config = pyproject(package).get("tool", {}).get("repo-review", {})
207+
ignore_pyproject: list[str] | dict[str, str] = config.get("ignore", [])
204208
select_checks = (
205-
select if select else frozenset(config.get("select", ())) | extend_select
206-
)
207-
skip_checks = (
208-
ignore if ignore else frozenset(config.get("ignore", ())) | extend_ignore
209-
)
209+
select if select else frozenset(config.get("select", ()))
210+
) | extend_select
211+
skip_checks = (ignore if ignore else frozenset(ignore_pyproject)) | extend_ignore
212+
skip_reasons = ignore_pyproject if isinstance(ignore_pyproject, dict) else {}
210213

211214
# Make a graph of the check's interdependencies
212215
graph: dict[str, Set[str]] = {
@@ -240,9 +243,14 @@ def process(
240243
result = None if completed[task_name] is None else not completed[task_name]
241244
doc = check.__doc__ or ""
242245
err_msg = completed[task_name] or ""
246+
skip_reason = ""
243247

244248
if not is_allowed(select_checks, skip_checks, task_name):
245-
continue
249+
key = name_matches(task_name, skip_reasons.keys())
250+
if not key or not skip_reasons.get(key, ""):
251+
continue
252+
result = None
253+
skip_reason = skip_reasons[key]
246254

247255
result_list.append(
248256
Result(
@@ -252,6 +260,7 @@ def process(
252260
result=result,
253261
err_msg=textwrap.dedent(err_msg),
254262
url=get_check_url(task_name, check),
263+
skip_reason=skip_reason,
255264
)
256265
)
257266

tests/test_self.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ def patch_entry_points(local_entry_points: object) -> None:
1111

1212

1313
def test_pyproject() -> None:
14-
families, results = repo_review.processor.process(Path())
14+
families, results = repo_review.processor.process(
15+
Path(), extend_ignore={"X", "PP303"}
16+
)
1517

1618
assert families == {
1719
"general": {},
@@ -26,7 +28,9 @@ def test_pyproject() -> None:
2628

2729

2830
def test_no_pyproject() -> None:
29-
families, results = repo_review.processor.process(Path("tests"))
31+
families, results = repo_review.processor.process(
32+
Path("tests"), extend_ignore={"X", "PP303"}
33+
)
3034

3135
assert families == {
3236
"general": {},
@@ -58,12 +62,13 @@ def test_empty_pyproject() -> None:
5862
"description": "Has flit_core.buildapi backend",
5963
"name": "PyProject",
6064
},
65+
"skipped": {},
6166
}
62-
assert len(results) == 9
67+
assert len(results) == 12
6368

6469
assert (
6570
sum(result.result is None for result in results if result.family == "pyproject")
66-
== 1
71+
== 2
6772
)
6873
assert (
6974
sum(result.result for result in results if isinstance(result.result, bool)) == 6
@@ -72,3 +77,6 @@ def test_empty_pyproject() -> None:
7277
sum(result.result is None for result in results if result.family == "general")
7378
== 0
7479
)
80+
assert sum(1 for result in results if result.skip_reason) == 3
81+
assert sum(1 for result in results if result.skip_reason == "One skip") == 1
82+
assert sum(1 for result in results if result.skip_reason == "Group skip") == 2

tests/test_utilities/pyproject.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,51 @@ def check(pyproject: dict[str, Any]) -> bool:
142142
return "minversion" in options and float(options["minversion"]) >= 6
143143

144144

145-
def repo_review_checks() -> dict[str, PyProject | General]:
146-
return {p.__name__: p() for p in PyProject.__subclasses__()} | {
145+
class PP999(PyProject):
146+
"Skipped check (single)"
147+
148+
@staticmethod
149+
def check() -> bool:
150+
"Not used"
151+
return False
152+
153+
154+
class X101:
155+
"Skipped check (multi)"
156+
157+
family = "skipped"
158+
159+
@staticmethod
160+
def check() -> bool:
161+
"Not used"
162+
return False
163+
164+
165+
class X102:
166+
"Skipped check (multi)"
167+
168+
family = "skipped"
169+
170+
@staticmethod
171+
def check() -> bool:
172+
"Not used"
173+
return False
174+
175+
176+
def repo_review_checks(
177+
pyproject: dict[str, Any],
178+
) -> dict[str, PyProject | General | X101 | X102]:
179+
ret = {p.__name__: p() for p in PyProject.__subclasses__()} | {
147180
p.__name__: p() for p in General.__subclasses__()
148181
}
182+
extra_checks = (
183+
pyproject.get("tool", {}).get("repo-review-local", {}).get("extra", False)
184+
)
185+
return (
186+
(ret | {"X101": X101()} | {"X102": X102()})
187+
if extra_checks
188+
else {k: v for k, v in ret.items() if k != "PP999"}
189+
)
149190

150191

151192
def repo_review_families(pyproject: dict[str, Any]) -> dict[str, dict[str, str]]:

tests/test_utilities/pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,11 @@ pyproject = "pyproject:repo_review_checks"
1616
pyproject = "pyproject:repo_review_families"
1717

1818
[tool.example]
19+
20+
21+
[tool.repo-review.ignore]
22+
"PP999" = "One skip"
23+
"X" = "Group skip"
24+
25+
[tool.repo-review-local]
26+
extra = true

0 commit comments

Comments
 (0)