repo-review 0.11.3__tar.gz → 0.12.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {repo_review-0.11.3 → repo_review-0.12.1}/.github/workflows/cd.yml +3 -0
  2. {repo_review-0.11.3 → repo_review-0.12.1}/.github/workflows/ci.yml +9 -1
  3. {repo_review-0.11.3 → repo_review-0.12.1}/.pre-commit-config.yaml +5 -5
  4. {repo_review-0.11.3 → repo_review-0.12.1}/PKG-INFO +8 -3
  5. {repo_review-0.11.3 → repo_review-0.12.1}/README.md +5 -1
  6. {repo_review-0.11.3 → repo_review-0.12.1}/docs/index.html +5 -5
  7. {repo_review-0.11.3 → repo_review-0.12.1}/docs/intro.md +18 -2
  8. {repo_review-0.11.3 → repo_review-0.12.1}/docs/webapp.js +9 -5
  9. {repo_review-0.11.3 → repo_review-0.12.1}/pyproject.toml +16 -3
  10. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/__main__.py +31 -1
  11. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/_version.py +2 -2
  12. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/checks.py +22 -8
  13. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/html.py +10 -8
  14. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/processor.py +39 -4
  15. {repo_review-0.11.3 → repo_review-0.12.1}/tests/test_self.py +12 -4
  16. {repo_review-0.11.3 → repo_review-0.12.1}/tests/test_utilities/pyproject.py +43 -2
  17. {repo_review-0.11.3 → repo_review-0.12.1}/tests/test_utilities/pyproject.toml +8 -0
  18. {repo_review-0.11.3 → repo_review-0.12.1}/.devcontainer/devcontainer.json +0 -0
  19. {repo_review-0.11.3 → repo_review-0.12.1}/.git_archival.txt +0 -0
  20. {repo_review-0.11.3 → repo_review-0.12.1}/.gitattributes +0 -0
  21. {repo_review-0.11.3 → repo_review-0.12.1}/.github/CONTRIBUTING.md +0 -0
  22. {repo_review-0.11.3 → repo_review-0.12.1}/.github/ISSUE_TEMPLATE/new-issue.md +0 -0
  23. {repo_review-0.11.3 → repo_review-0.12.1}/.github/dependabot.yml +0 -0
  24. {repo_review-0.11.3 → repo_review-0.12.1}/.github/release.yml +0 -0
  25. {repo_review-0.11.3 → repo_review-0.12.1}/.gitignore +0 -0
  26. {repo_review-0.11.3 → repo_review-0.12.1}/.pre-commit-hooks.yaml +0 -0
  27. {repo_review-0.11.3 → repo_review-0.12.1}/.readthedocs.yaml +0 -0
  28. {repo_review-0.11.3 → repo_review-0.12.1}/LICENSE +0 -0
  29. {repo_review-0.11.3 → repo_review-0.12.1}/action.yml +0 -0
  30. {repo_review-0.11.3 → repo_review-0.12.1}/docs/.nojekyll +0 -0
  31. {repo_review-0.11.3 → repo_review-0.12.1}/docs/api/repo_review.resources.rst +0 -0
  32. {repo_review-0.11.3 → repo_review-0.12.1}/docs/api/repo_review.rst +0 -0
  33. {repo_review-0.11.3 → repo_review-0.12.1}/docs/changelog.md +0 -0
  34. {repo_review-0.11.3 → repo_review-0.12.1}/docs/checks.md +0 -0
  35. {repo_review-0.11.3 → repo_review-0.12.1}/docs/cli.md +0 -0
  36. {repo_review-0.11.3 → repo_review-0.12.1}/docs/conf.py +0 -0
  37. {repo_review-0.11.3 → repo_review-0.12.1}/docs/families.md +0 -0
  38. {repo_review-0.11.3 → repo_review-0.12.1}/docs/fixtures.md +0 -0
  39. {repo_review-0.11.3 → repo_review-0.12.1}/docs/index.md +0 -0
  40. {repo_review-0.11.3 → repo_review-0.12.1}/docs/plugins.md +0 -0
  41. {repo_review-0.11.3 → repo_review-0.12.1}/docs/programmatic.md +0 -0
  42. {repo_review-0.11.3 → repo_review-0.12.1}/docs/webapp.md +0 -0
  43. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/__init__.py +0 -0
  44. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/_compat/__init__.py +0 -0
  45. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/_compat/importlib/__init__.py +0 -0
  46. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/_compat/importlib/resources/__init__.py +0 -0
  47. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/_compat/importlib/resources/abc.py +0 -0
  48. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/_compat/tomllib.py +0 -0
  49. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/_compat/typing.py +0 -0
  50. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/_version.pyi +0 -0
  51. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/families.py +0 -0
  52. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/fixtures.py +0 -0
  53. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/ghpath.py +0 -0
  54. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/py.typed +0 -0
  55. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/resources/__init__.py +0 -0
  56. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/resources/repo-review.schema.json +0 -0
  57. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/schema.py +0 -0
  58. {repo_review-0.11.3 → repo_review-0.12.1}/src/repo_review/testing.py +0 -0
  59. {repo_review-0.11.3 → repo_review-0.12.1}/tests/conftest.py +0 -0
  60. {repo_review-0.11.3 → repo_review-0.12.1}/tests/test_checks.py +0 -0
  61. {repo_review-0.11.3 → repo_review-0.12.1}/tests/test_cmd.py +0 -0
  62. {repo_review-0.11.3 → repo_review-0.12.1}/tests/test_depends.py +0 -0
  63. {repo_review-0.11.3 → repo_review-0.12.1}/tests/test_families.py +0 -0
  64. {repo_review-0.11.3 → repo_review-0.12.1}/tests/test_fixtures.py +0 -0
  65. {repo_review-0.11.3 → repo_review-0.12.1}/tests/test_multi.py +0 -0
  66. {repo_review-0.11.3 → repo_review-0.12.1}/tests/test_package.py +0 -0
@@ -6,6 +6,8 @@ on:
6
6
  types:
7
7
  - published
8
8
 
9
+ permissions: {}
10
+
9
11
  jobs:
10
12
  dist:
11
13
  name: Distribution build
@@ -15,6 +17,7 @@ jobs:
15
17
  - uses: actions/checkout@v4
16
18
  with:
17
19
  fetch-depth: 0
20
+ persist-credentials: false
18
21
 
19
22
  - uses: hynek/build-and-inspect-python-package@v2
20
23
 
@@ -17,12 +17,16 @@ concurrency:
17
17
  env:
18
18
  FORCE_COLOR: 3
19
19
 
20
+ permissions: {}
21
+
20
22
  jobs:
21
23
  pre-commit:
22
24
  name: Format
23
25
  runs-on: ubuntu-latest
24
26
  steps:
25
27
  - uses: actions/checkout@v4
28
+ with:
29
+ persist-credentials: false
26
30
  - uses: actions/setup-python@v5
27
31
  with:
28
32
  python-version: "3.x"
@@ -42,6 +46,7 @@ jobs:
42
46
  - uses: actions/checkout@v4
43
47
  with:
44
48
  fetch-depth: 0
49
+ persist-credentials: false
45
50
 
46
51
  - uses: actions/setup-python@v5
47
52
  with:
@@ -69,6 +74,7 @@ jobs:
69
74
  - uses: actions/checkout@v4
70
75
  with:
71
76
  fetch-depth: 0
77
+ persist-credentials: false
72
78
 
73
79
  - uses: hynek/build-and-inspect-python-package@v2
74
80
 
@@ -79,6 +85,7 @@ jobs:
79
85
  - uses: actions/checkout@v4
80
86
  with:
81
87
  fetch-depth: 0
88
+ persist-credentials: false
82
89
 
83
90
  - name: Setup uv
84
91
  uses: yezz123/setup-uv@v4
@@ -108,8 +115,9 @@ jobs:
108
115
  - uses: actions/checkout@v4
109
116
  with:
110
117
  fetch-depth: 0
118
+ persist-credentials: false
111
119
 
112
120
  - name: Run repo-review action
113
121
  uses: ./
114
122
  with:
115
- plugins: sp-repo-review==2024.08.19
123
+ plugins: sp-repo-review==2025.01.22
@@ -11,7 +11,7 @@ repos:
11
11
  additional_dependencies: [black==24.*]
12
12
 
13
13
  - repo: https://github.com/astral-sh/ruff-pre-commit
14
- rev: "v0.8.3"
14
+ rev: "v0.9.4"
15
15
  hooks:
16
16
  - id: ruff
17
17
  args: ["--fix", "--show-fixes"]
@@ -45,7 +45,7 @@ repos:
45
45
  - id: rst-inline-touching-normal
46
46
 
47
47
  - repo: https://github.com/pre-commit/mirrors-mypy
48
- rev: v1.13.0
48
+ rev: v1.14.1
49
49
  hooks:
50
50
  - id: mypy
51
51
  files: (src|web|tests)
@@ -60,7 +60,7 @@ repos:
60
60
  - types-PyYAML
61
61
 
62
62
  - repo: https://github.com/codespell-project/codespell
63
- rev: v2.3.0
63
+ rev: v2.4.1
64
64
  hooks:
65
65
  - id: codespell
66
66
  args: ["-Lhist,absense", "-w"]
@@ -79,12 +79,12 @@ repos:
79
79
  exclude: .pre-commit-config.yaml
80
80
 
81
81
  - repo: https://github.com/henryiii/validate-pyproject-schema-store
82
- rev: 2024.11.25
82
+ rev: 2025.02.03
83
83
  hooks:
84
84
  - id: validate-pyproject
85
85
 
86
86
  - repo: https://github.com/python-jsonschema/check-jsonschema
87
- rev: 0.30.0
87
+ rev: 0.31.1
88
88
  hooks:
89
89
  - id: check-dependabot
90
90
  - id: check-github-workflows
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: repo_review
3
- Version: 0.11.3
3
+ Version: 0.12.1
4
4
  Summary: Framework that can run checks on repos
5
5
  Project-URL: Changelog, https://github.com/scientific-python/repo-review/releases
6
6
  Project-URL: Demo, https://scientific-python.github.io/repo-review
@@ -8,6 +8,7 @@ Project-URL: Documentation, https://repo-review.readthedocs.io
8
8
  Project-URL: Homepage, https://repo-review.readthedocs.io
9
9
  Project-URL: Source, https://github.com/scientific-python/repo-review
10
10
  Author-email: Henry Schreiner <henryfs@princeton.edu>
11
+ License-File: LICENSE
11
12
  Classifier: Development Status :: 4 - Beta
12
13
  Classifier: Environment :: Console
13
14
  Classifier: Environment :: WebAssembly :: Emscripten
@@ -101,8 +102,12 @@ select = ["A", "B", "C100"]
101
102
  ignore = ["A100"]
102
103
  ```
103
104
 
105
+ The ignore list can also be a table, with reasons for values.
106
+
104
107
  If `--select` or `--ignore` are given on the command line, they will override
105
- the `pyproject.toml` config.
108
+ the `pyproject.toml` config. You can use `--extend-select` and `--extend-ignore`
109
+ on the command line to extend the `pyproject.toml` config. These CLI options
110
+ are comma separated.
106
111
 
107
112
  ## Comparison to other frameworks
108
113
 
@@ -64,8 +64,12 @@ select = ["A", "B", "C100"]
64
64
  ignore = ["A100"]
65
65
  ```
66
66
 
67
+ The ignore list can also be a table, with reasons for values.
68
+
67
69
  If `--select` or `--ignore` are given on the command line, they will override
68
- the `pyproject.toml` config.
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.
69
73
 
70
74
  ## Comparison to other frameworks
71
75
 
@@ -6,7 +6,7 @@
6
6
  content="initial-scale=1, width=device-width"
7
7
  />
8
8
  <script
9
- src="https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.js"
9
+ src="https://cdn.jsdelivr.net/pyodide/v0.27.1/full/pyodide.js"
10
10
  crossorigin
11
11
  ></script>
12
12
  <!-- Production -->
@@ -64,10 +64,10 @@
64
64
  <App
65
65
  header={true}
66
66
  deps={[
67
- "repo-review~=0.11.0",
68
- "sp-repo-review==2024.08.19",
69
- "validate-pyproject-schema-store== 2024.08.19",
70
- "validate-pyproject[all]~=0.19.0",
67
+ "repo-review~=0.12.0",
68
+ "sp-repo-review==2025.01.22",
69
+ "validate-pyproject-schema-store==2025.02.03",
70
+ "validate-pyproject[all]~=0.22.0",
71
71
  ]}
72
72
  />,
73
73
  );
@@ -51,10 +51,26 @@ You can explicitly list checks to select or skip in your `pyproject.toml`:
51
51
 
52
52
  ```toml
53
53
  [tool.repo-review]
54
- select = ["..."]
55
- ignore = ["..."]
54
+ select = ["A", "B", "C100"]
55
+ ignore = ["A100"]
56
56
  ```
57
57
 
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
+
58
74
  ## Pre-commit
59
75
 
60
76
  You can also use this from pre-commit:
@@ -93,7 +93,7 @@ function Results(props) {
93
93
  variant="body2"
94
94
  color="text.disabled"
95
95
  >
96
- {" [skipped]"}
96
+ {` [skipped] ${result.skip_reason}`}
97
97
  </MaterialUI.Typography>
98
98
  );
99
99
  const msg = (
@@ -200,6 +200,7 @@ class App extends React.Component {
200
200
  msg: `<p>${DEFAULT_MSG}</p><h4>Packages:</h4> ${deps_str}`,
201
201
  progress: false,
202
202
  err_msg: "",
203
+ skip_reason: "",
203
204
  url: "",
204
205
  };
205
206
  this.pyodide_promise = prepare_pyodide(props.deps);
@@ -231,15 +232,17 @@ class App extends React.Component {
231
232
  families_checks = pyodide.runPython(`
232
233
  from repo_review.processor import process, md_as_html
233
234
  from repo_review.ghpath import GHPath
235
+ from dataclasses import replace
234
236
 
235
237
  package = GHPath(repo="${state.repo}", branch="${state.branch}")
236
- result = process(package)
238
+ families, checks = process(package)
237
239
 
238
- for v in result[0].values():
240
+ for v in families.values():
239
241
  if v.get("description"):
240
242
  v["description"] = md_as_html(v["description"])
243
+ checks = [replace(v, err_msg=md_as_html(v.err_msg), skip_reason=md_as_html(v.skip_reason)) for v in checks]
241
244
 
242
- result
245
+ (families, checks)
243
246
  `);
244
247
  } catch (e) {
245
248
  if (e.message.includes("KeyError: 'tree'")) {
@@ -276,8 +279,9 @@ class App extends React.Component {
276
279
  name: val.name.toString(),
277
280
  description: val.description.toString(),
278
281
  state: val.result,
279
- err_msg: val.err_as_html().toString(),
282
+ err_msg: val.err_msg.toString(),
280
283
  url: val.url.toString(),
284
+ skip_reason: val.skip_reason.toString(),
281
285
  });
282
286
  }
283
287
 
@@ -64,11 +64,15 @@ cli = [
64
64
  [dependency-groups]
65
65
  test = [
66
66
  "pytest >=7",
67
- "sp-repo-review >=2024.08.19",
67
+ "sp-repo-review >=2025.01.22",
68
68
  "validate-pyproject >=0.14",
69
69
  ]
70
- dev = [
70
+ cov = [
71
71
  { include-group = "test" },
72
+ "pytest-cov",
73
+ ]
74
+ dev = [
75
+ { include-group = "cov" },
72
76
  "sp-repo-review[cli]",
73
77
  "validate-pyproject-schema-store[all]",
74
78
  ]
@@ -96,7 +100,7 @@ features = ["cli"]
96
100
  # duplicated since hatch doesn't support groups yet
97
101
  dependencies = [
98
102
  "pytest >=7",
99
- "sp-repo-review >=2024.08.19",
103
+ "sp-repo-review >=2025.01.22",
100
104
  "validate-pyproject >=0.14",
101
105
  ]
102
106
  env-vars.PYTHONWARNDEFAULTENCODING = "1"
@@ -242,3 +246,12 @@ typing-modules = ["repo_review._compat.typing"]
242
246
  [tool.ruff.lint.per-file-ignores]
243
247
  "src/repo_review/_compat/**.py" = ["TID251"]
244
248
  "src/**/__main__.py" = ["T20"]
249
+
250
+ [tool.coverage]
251
+ report.exclude_also = [
252
+ "def __dir__()",
253
+ "if TYPE_CHECKING:",
254
+ '\.\.\.',
255
+ ]
256
+ report.show_missing = true
257
+ report.skip_empty = true
@@ -163,6 +163,13 @@ def rich_printer(
163
163
  msg.append(rich.text.Text.from_markup(description, style=style))
164
164
  if result.result is None:
165
165
  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)
166
173
  tree.add(msg)
167
174
  elif result.result:
168
175
  msg.append(rich.text.Text.from_markup(" :white_check_mark:"))
@@ -315,6 +322,16 @@ def _remote_path_processor(package: Path) -> Path | GHPath:
315
322
  help="Ignore a check or checks, comma separated.",
316
323
  default="",
317
324
  )
325
+ @click.option(
326
+ "--extend-select",
327
+ help="Checks to run in addition to the ones selected.",
328
+ default="",
329
+ )
330
+ @click.option(
331
+ "--extend-ignore",
332
+ help="Checks to ignore in addition to the ones ignored.",
333
+ default="",
334
+ )
318
335
  @click.option(
319
336
  "--package-dir",
320
337
  "-p",
@@ -327,6 +344,8 @@ def main(
327
344
  stderr_fmt: Formats | None,
328
345
  select: str,
329
346
  ignore: str,
347
+ extend_select: str,
348
+ extend_ignore: str,
330
349
  package_dir: str,
331
350
  show: Show,
332
351
  ) -> None:
@@ -351,6 +370,8 @@ def main(
351
370
  stderr_fmt,
352
371
  select,
353
372
  ignore,
373
+ extend_select,
374
+ extend_ignore,
354
375
  package_dir,
355
376
  add_header=len(packages) > 1,
356
377
  show=show,
@@ -378,6 +399,8 @@ def on_each(
378
399
  stderr_fmt: Literal["rich", "json", "html", "svg"] | None,
379
400
  select: str,
380
401
  ignore: str,
402
+ extend_select: str,
403
+ extend_ignore: str,
381
404
  package_dir: str,
382
405
  *,
383
406
  add_header: bool,
@@ -387,6 +410,8 @@ def on_each(
387
410
 
388
411
  ignore_list = {x.strip() for x in ignore.split(",") if x}
389
412
  select_list = {x.strip() for x in select.split(",") if x}
413
+ extend_ignore_list = {x.strip() for x in extend_ignore.split(",") if x}
414
+ extend_select_list = {x.strip() for x in extend_select.split(",") if x}
390
415
 
391
416
  collected = collect_all(package, subdir=package_dir)
392
417
  if len(collected.checks) == 0:
@@ -407,7 +432,12 @@ def on_each(
407
432
  header = package.name
408
433
 
409
434
  families, processed = process(
410
- base_package, select=select_list, ignore=ignore_list, subdir=package_dir
435
+ base_package,
436
+ select=select_list,
437
+ ignore=ignore_list,
438
+ extend_select=extend_select_list,
439
+ extend_ignore=extend_ignore_list,
440
+ subdir=package_dir,
411
441
  )
412
442
 
413
443
  status: Status = "passed" if processed else "empty"
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.11.3'
16
- __version_tuple__ = version_tuple = (0, 11, 3)
15
+ __version__ = version = '0.12.1'
16
+ __version_tuple__ = version_tuple = (0, 12, 1)
@@ -6,7 +6,7 @@ from typing import Any, Protocol
6
6
 
7
7
  from .fixtures import apply_fixtures
8
8
 
9
- __all__ = ["Check", "collect_checks", "get_check_url", "is_allowed"]
9
+ __all__ = ["Check", "collect_checks", "get_check_url", "is_allowed", "name_matches"]
10
10
 
11
11
 
12
12
  def __dir__() -> list[str]:
@@ -70,6 +70,25 @@ def collect_checks(fixtures: Mapping[str, Any]) -> dict[str, Check]:
70
70
  }
71
71
 
72
72
 
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
+
73
92
  def is_allowed(select: Set[str], ignore: Set[str], name: str) -> bool:
74
93
  """
75
94
  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:
82
101
 
83
102
  :return: True if this check is allowed, False otherwise.
84
103
  """
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:
91
105
  return False
92
106
 
93
- return name not in ignore and name.rstrip("0123456789") not in ignore
107
+ return not name_matches(name, ignore)
94
108
 
95
109
 
96
110
  def get_check_url(name: str, check: Check) -> str:
@@ -6,10 +6,8 @@ import io
6
6
  import typing
7
7
  from collections.abc import Mapping, Sequence
8
8
 
9
- import markdown_it
10
-
11
9
  from .families import Family, get_family_description, get_family_name
12
- from .processor import Result
10
+ from .processor import Result, md_as_html
13
11
 
14
12
  if typing.TYPE_CHECKING:
15
13
  from .__main__ import Status
@@ -37,16 +35,15 @@ def to_html(
37
35
  """
38
36
  out = io.StringIO()
39
37
  print = functools.partial(builtins.print, file=out)
40
- md = markdown_it.MarkdownIt()
41
38
 
42
39
  for family in families:
43
40
  family_name = get_family_name(families, family)
44
41
  family_description = get_family_description(families, family)
45
- family_results = [r for r in processed if r.family == family]
42
+ family_results = [r.md_as_html() for r in processed if r.family == family]
46
43
  if family_results or family_description:
47
44
  print(f"<h3>{family_name}</h3>")
48
45
  if family_description:
49
- print(md.render(family_description).strip())
46
+ print("<p>", md_as_html(family_description), "</p>")
50
47
  if family_results:
51
48
  print("<table>")
52
49
  print(
@@ -61,7 +58,7 @@ def to_html(
61
58
  else "red"
62
59
  )
63
60
  icon = (
64
- "&#9888;&#65039;"
61
+ ("&#128311;" if result.skip_reason else "&#9888;&#65039;")
65
62
  if result.result is None
66
63
  else "&#9989;"
67
64
  if result.result
@@ -79,6 +76,11 @@ def to_html(
79
76
  if result.url
80
77
  else result.description
81
78
  )
79
+ if result.skip_reason:
80
+ description += (
81
+ f'<br/><span style="color:DarkKhaki;"><b>Skipped:</b> '
82
+ f"<em>{result.skip_reason}</em></span>"
83
+ )
82
84
  print(f'<tr style="color: {color};">')
83
85
  print(f'<td><span role="img" aria-label="{result_txt}">{icon}</span></td>')
84
86
  print(f'<td nowrap="nowrap">{result.name}</td>')
@@ -88,7 +90,7 @@ def to_html(
88
90
  print("<td>")
89
91
  print(description)
90
92
  print("<br/>")
91
- print(md.render(result.err_msg))
93
+ print(result.err_msg)
92
94
  print("</td>")
93
95
  print("</tr>")
94
96
  if family_results:
@@ -3,11 +3,12 @@ from __future__ import annotations
3
3
  import copy
4
4
  import dataclasses
5
5
  import graphlib
6
+ import sys
6
7
  import textwrap
7
8
  import typing
8
9
  import warnings
9
10
  from collections.abc import Mapping, Set
10
- from typing import Any, TypeVar
11
+ from typing import TYPE_CHECKING, Any, TypeVar
11
12
 
12
13
  import markdown_it
13
14
 
@@ -17,12 +18,19 @@ from .checks import (
17
18
  collect_checks,
18
19
  get_check_url,
19
20
  is_allowed,
21
+ name_matches,
20
22
  process_result_bool,
21
23
  )
22
24
  from .families import Family, collect_families
23
25
  from .fixtures import apply_fixtures, collect_fixtures, compute_fixtures, pyproject
24
26
  from .ghpath import EmptyTraversable
25
27
 
28
+ if TYPE_CHECKING:
29
+ if sys.version_info >= (3, 11):
30
+ from typing import Self
31
+ else:
32
+ from typing_extensions import Self
33
+
26
34
  __all__ = [
27
35
  "CollectionReturn",
28
36
  "ProcessReturn",
@@ -63,6 +71,7 @@ class ResultDict(typing.TypedDict):
63
71
  result: bool | None #: The result, None means skip
64
72
  err_msg: str #: The error message if the result is false, in markdown format
65
73
  url: str #: An optional URL (empty string if missing)
74
+ skip_reason: str #: The reason for the skip, if given (empty string if not)
66
75
 
67
76
 
68
77
  @dataclasses.dataclass(frozen=True, kw_only=True)
@@ -75,15 +84,29 @@ class Result:
75
84
  name: str #: The name of the check
76
85
  description: str #: The short description of what the check looks for
77
86
  result: bool | None #: The result, None means skip
87
+ skip_reason: str = "" #: The reason for the skip, if given
78
88
  err_msg: str = "" #: The error message if the result is false, in markdown format
79
89
  url: str = "" #: An optional URL (empty string if missing)
80
90
 
81
91
  def err_as_html(self) -> str:
82
92
  """
83
93
  Produces HTML from the error message, assuming it is in markdown.
94
+ Deprecated, use :meth:`md_as_html` directly instead.
84
95
  """
85
96
  return md_as_html(self.err_msg)
86
97
 
98
+ def md_as_html(self) -> Self:
99
+ """
100
+ Process fields that are assumed to be markdown.
101
+
102
+ .. versionadded:: 0.12.1
103
+ """
104
+ return dataclasses.replace(
105
+ self,
106
+ err_msg=md_as_html(self.err_msg),
107
+ skip_reason=md_as_html(self.skip_reason),
108
+ )
109
+
87
110
 
88
111
  class ProcessReturn(typing.NamedTuple):
89
112
  """
@@ -179,6 +202,8 @@ def process(
179
202
  *,
180
203
  select: Set[str] = frozenset(),
181
204
  ignore: Set[str] = frozenset(),
205
+ extend_select: Set[str] = frozenset(),
206
+ extend_ignore: Set[str] = frozenset(),
182
207
  subdir: str = "",
183
208
  ) -> ProcessReturn:
184
209
  """
@@ -199,8 +224,12 @@ def process(
199
224
 
200
225
  # Collect our own config
201
226
  config = pyproject(package).get("tool", {}).get("repo-review", {})
202
- select_checks = select if select else frozenset(config.get("select", ()))
203
- skip_checks = ignore if ignore else frozenset(config.get("ignore", ()))
227
+ ignore_pyproject: list[str] | dict[str, str] = config.get("ignore", [])
228
+ select_checks = (
229
+ select if select else frozenset(config.get("select", ()))
230
+ ) | extend_select
231
+ skip_checks = (ignore if ignore else frozenset(ignore_pyproject)) | extend_ignore
232
+ skip_reasons = ignore_pyproject if isinstance(ignore_pyproject, dict) else {}
204
233
 
205
234
  # Make a graph of the check's interdependencies
206
235
  graph: dict[str, Set[str]] = {
@@ -234,9 +263,14 @@ def process(
234
263
  result = None if completed[task_name] is None else not completed[task_name]
235
264
  doc = check.__doc__ or ""
236
265
  err_msg = completed[task_name] or ""
266
+ skip_reason = ""
237
267
 
238
268
  if not is_allowed(select_checks, skip_checks, task_name):
239
- continue
269
+ key = name_matches(task_name, skip_reasons.keys())
270
+ if not key or not skip_reasons.get(key, ""):
271
+ continue
272
+ result = None
273
+ skip_reason = skip_reasons[key]
240
274
 
241
275
  result_list.append(
242
276
  Result(
@@ -246,6 +280,7 @@ def process(
246
280
  result=result,
247
281
  err_msg=textwrap.dedent(err_msg),
248
282
  url=get_check_url(task_name, check),
283
+ skip_reason=skip_reason,
249
284
  )
250
285
  )
251
286
 
@@ -11,7 +11,9 @@ def patch_entry_points(local_entry_points: object) -> None:
11
11
 
12
12
 
13
13
  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
+ )
15
17
 
16
18
  assert families == {
17
19
  "general": {},
@@ -26,7 +28,9 @@ def test_pyproject() -> None:
26
28
 
27
29
 
28
30
  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
+ )
30
34
 
31
35
  assert families == {
32
36
  "general": {},
@@ -58,12 +62,13 @@ def test_empty_pyproject() -> None:
58
62
  "description": "Has flit_core.buildapi backend",
59
63
  "name": "PyProject",
60
64
  },
65
+ "skipped": {},
61
66
  }
62
- assert len(results) == 9
67
+ assert len(results) == 12
63
68
 
64
69
  assert (
65
70
  sum(result.result is None for result in results if result.family == "pyproject")
66
- == 1
71
+ == 2
67
72
  )
68
73
  assert (
69
74
  sum(result.result for result in results if isinstance(result.result, bool)) == 6
@@ -72,3 +77,6 @@ def test_empty_pyproject() -> None:
72
77
  sum(result.result is None for result in results if result.family == "general")
73
78
  == 0
74
79
  )
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
@@ -142,10 +142,51 @@ class PP302(PyProject):
142
142
  return "minversion" in options and float(options["minversion"]) >= 6
143
143
 
144
144
 
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__()} | {
147
180
  p.__name__: p() for p in General.__subclasses__()
148
181
  }
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
+ )
149
190
 
150
191
 
151
192
  def repo_review_families(pyproject: dict[str, Any]) -> dict[str, dict[str, str]]:
@@ -16,3 +16,11 @@ pyproject = "pyproject:repo_review_checks"
16
16
  pyproject = "pyproject:repo_review_families"
17
17
 
18
18
  [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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes