readability-cli 0.8.6__tar.gz → 0.9.0__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 (40) hide show
  1. {readability_cli-0.8.6 → readability_cli-0.9.0}/.github/workflows/update-guides.yml +1 -1
  2. {readability_cli-0.8.6 → readability_cli-0.9.0}/PKG-INFO +40 -38
  3. {readability_cli-0.8.6 → readability_cli-0.9.0}/README.md +39 -37
  4. {readability_cli-0.8.6 → readability_cli-0.9.0}/pyproject.toml +4 -7
  5. readability_cli-0.9.0/readability/__init__.py +0 -0
  6. readability_cli-0.9.0/readability/checking.py +265 -0
  7. readability_cli-0.9.0/readability/cli.py +264 -0
  8. readability_cli-0.9.0/readability/guide.py +250 -0
  9. readability_cli-0.9.0/readability/outline.py +681 -0
  10. readability_cli-0.9.0/readability/tools.py +407 -0
  11. readability_cli-0.9.0/test_checking.py +1427 -0
  12. readability_cli-0.9.0/test_guide.py +223 -0
  13. readability_cli-0.9.0/test_outline.py +952 -0
  14. readability_cli-0.9.0/test_package.py +25 -0
  15. {readability_cli-0.8.6 → readability_cli-0.9.0}/uv.lock +1 -1
  16. readability_cli-0.8.6/.prettierignore +0 -2
  17. readability_cli-0.8.6/readability.py +0 -1815
  18. readability_cli-0.8.6/test_readability.py +0 -2450
  19. {readability_cli-0.8.6 → readability_cli-0.9.0}/.github/workflows/ci.yml +0 -0
  20. {readability_cli-0.8.6 → readability_cli-0.9.0}/.github/workflows/publish.yml +0 -0
  21. {readability_cli-0.8.6 → readability_cli-0.9.0}/.gitignore +0 -0
  22. {readability_cli-0.8.6 → readability_cli-0.9.0}/.python-version +0 -0
  23. {readability_cli-0.8.6 → readability_cli-0.9.0}/LICENSE +0 -0
  24. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/configs/biome-default.json +0 -0
  25. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/configs/pyrefly.toml +0 -0
  26. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/configs/ruff.toml +0 -0
  27. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/guides/Rguide.md +0 -0
  28. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/guides/cppguide.md +0 -0
  29. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/guides/csharp-style.md +0 -0
  30. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/guides/docguide-style.md +0 -0
  31. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/guides/go-guide.md +0 -0
  32. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/guides/htmlcssguide.md +0 -0
  33. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/guides/javaguide.md +0 -0
  34. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/guides/jsguide.md +0 -0
  35. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/guides/jsoncstyleguide.md +0 -0
  36. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/guides/objcguide.md +0 -0
  37. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/guides/pyguide.md +0 -0
  38. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/guides/shellguide.md +0 -0
  39. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/guides/tsguide.md +0 -0
  40. {readability_cli-0.8.6 → readability_cli-0.9.0/readability}/guides/vimscriptguide.md +0 -0
@@ -30,4 +30,4 @@ jobs:
30
30
  uses: stefanzweifel/git-auto-commit-action@v5
31
31
  with:
32
32
  commit_message: "chore: update style guides [skip ci]"
33
- file_pattern: "guides/*.md"
33
+ file_pattern: "readability/guides/*.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: readability-cli
3
- Version: 0.8.6
3
+ Version: 0.9.0
4
4
  Summary: A CLI to lint, format, and type-check code with Google-style defaults, and pull Google style guides in markdown format.
5
5
  Project-URL: Homepage, https://github.com/owahltinez/readability
6
6
  Project-URL: Repository, https://github.com/owahltinez/readability
@@ -23,9 +23,9 @@ quick access to style conventions without browsing HTML pages.
23
23
  ## Features
24
24
 
25
25
  - **Linting & Formatting**: A `check` command that automatically detects and
26
- runs relevant tools (Ruff, Pyrefly, Biome, Prettier, gofmt) for your project.
27
- - **Sensible Defaults**: Google-style defaults for Ruff, Pyrefly, Biome, and
28
- Prettier are used automatically when a project does not define its own.
26
+ runs relevant tools (Ruff, Pyrefly, Biome, and gofmt) for your project.
27
+ - **Predictable Ownership**: Every supported format has fixed tools, so
28
+ configuration customizes checks without changing which formatter runs.
29
29
  - **Style Guides**: A `guide` command that fetches the latest Google style
30
30
  guides (Python, Shell, C++, Java, JS/TS, Go, etc.) converted to Markdown, and
31
31
  outlines, addresses, and searches them by section rather than serving 200 KB
@@ -78,8 +78,8 @@ uv run readability sync
78
78
  ## Checking and Formatting
79
79
 
80
80
  The `check` command identifies and runs relevant linting and formatting tools
81
- based on file extensions and the presence of configuration files (triggers) in
82
- your project root:
81
+ solely from file extensions. Configuration can customize the assigned tools,
82
+ but cannot opt another tool into a format:
83
83
 
84
84
  ```bash
85
85
  # Run checks on the current directory
@@ -92,21 +92,22 @@ readability check src/ tests/ main.py
92
92
  readability check . --fix
93
93
  ```
94
94
 
95
- ### Supported Tools
95
+ ### Supported Formats
96
96
 
97
- | Tool | Supported Extensions | Config Needed | Fetched By |
98
- | ------------ | --------------------------------------------------------------------------------------- | --------------------------------- | ---------- |
99
- | **Ruff** | `.py` | none, bundled | `uvx` |
100
- | **Pyrefly** | `.py` | none, bundled | `uvx` |
101
- | **Biome** | `.js`, `.ts`, `.jsx`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` | none, bundled | `npx` |
102
- | **Prettier** | `.js`, `.ts`, `.jsx`, `.tsx`, `.json`, `.css`, `.scss`, `.html`, `.md`, `.yml`, `.yaml` | none for Markdown, YAML, and SCSS | `npx` |
103
- | **gofmt** | `.go` | `go.mod` | — |
97
+ | Formats | Owners | Project configuration |
98
+ | ---------------------------------------------------------------- | --------------------------------------- | ------------------------------------- |
99
+ | `.py` | Ruff lint/format; Pyrefly type checking | Ruff and Pyrefly native configuration |
100
+ | `.js`, `.jsx`, `.ts`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` | Biome | `biome.json` or `biome.jsonc` |
101
+ | `.go` | gofmt | None |
104
102
 
105
- **You do not need to install any of these.** Each is resolved in order: a
106
- project-local install (`node_modules/.bin`, `.venv/bin`) first, then whatever is
107
- on your `PATH`, and only then fetched by `uvx` or `npx`. The runners cache what
108
- they download, so the fetch happens once per machine and later runs are served
109
- from disk and work offline.
103
+ Markdown, YAML, SCSS, JSONL, and extensions not listed above are unsupported.
104
+ An unsupported-only path reports that nothing was checked and exits
105
+ successfully.
106
+
107
+ Biome first checks a project's `node_modules/.bin`. Ruff, Pyrefly, and Biome
108
+ then use `PATH`, followed by `uvx` for the Python tools or `npx` for Biome. The
109
+ runners cache downloads, so subsequent runs work offline. Gofmt ships with Go
110
+ and must be available on `PATH`.
110
111
 
111
112
  That keeps this package at ~4 MB rather than the ~54 MB it would take to carry
112
113
  Ruff and Pyrefly itself — a cost that would fall on everyone using only `guide`.
@@ -123,18 +124,17 @@ over a fetched one.
123
124
  Set `UV_OFFLINE=1` to forbid fetching. A tool that then cannot be reached fails
124
125
  the run rather than passing it.
125
126
 
126
- Ruff, Pyrefly, and Biome ship with bundled configurations, so they run on any
127
- file they handle without the project arranging anything. Prettier does the same
128
- for Markdown, YAML, and SCSS; a Prettier configuration opts its overlapping web
129
- formats into Prettier too. Gofmt waits for a `go.mod` file.
127
+ Ruff, Pyrefly, and Biome ship with bundled configurations, so they run on every
128
+ file they own without project setup. Gofmt runs on `.go` files even when there
129
+ is no `go.mod`.
130
130
 
131
131
  A tool that could not be reached at all is never skipped quietly:
132
132
 
133
133
  ```bash
134
134
  # Some tools ran, so the result stands, but coverage was partial
135
135
  $ readability check src/
136
- Warning: not installed, so not run: prettier.
137
- No findings in 1 path(s) (ruff, pyrefly).
136
+ Warning: not installed, so not run: pyrefly.
137
+ No findings in 1 path(s) (ruff).
138
138
 
139
139
  # Nothing ran, so there is no result to report
140
140
  $ readability check src/
@@ -157,7 +157,7 @@ or exit-code decisions:
157
157
  ```python
158
158
  from pathlib import Path
159
159
 
160
- from readability import check_paths
160
+ from readability.checking import check_paths
161
161
 
162
162
  report = check_paths(["src", Path("tests")], project_root=Path.cwd())
163
163
  if report.findings or report.failed or not report.ran:
@@ -171,22 +171,23 @@ raises `FileNotFoundError` rather than being misreported as a finding. Relative
171
171
  paths remain relative to the process working directory; `project_root` controls
172
172
  configuration discovery only.
173
173
 
174
- ### Default Configurations
174
+ ### Configuring Formats
175
175
 
176
176
  Ruff and Pyrefly defaults follow the
177
177
  [Google Python style guide](https://google.github.io/styleguide/pyguide.html):
178
178
  80-column lines, Google docstrings, import ordering, and full type checking. The
179
179
  Biome default applies the 80-column lines and two-space indentation of the
180
180
  [Google JavaScript style guide](https://google.github.io/styleguide/jsguide.html)
181
- and enables Biome's recommended lint rules. Prettier wraps prose at 80 columns,
182
- following the
183
- [Google Markdown style guide](https://google.github.io/styleguide/docguide/style.html#character-line-limit).
184
- These defaults apply only when the project does not define its own
185
- configuration. To override them, add a `[tool.ruff]` or `[tool.pyrefly]` section
186
- to your `pyproject.toml`, or a dedicated `ruff.toml`, `pyrefly.toml`,
187
- `biome.json`, `biome.jsonc`, or Prettier configuration any project-level
188
- configuration takes full precedence. The bundled Biome file requires Biome 2.5
189
- or later, matching the version floor used by the fallback runner.
181
+ and enables Biome's recommended lint rules. Project `biome.json` and
182
+ `biome.jsonc` files replace those bundled defaults for Biome-owned formats.
183
+
184
+ For Python, add `[tool.ruff]` or `[tool.pyrefly]` to `pyproject.toml`, or use
185
+ `ruff.toml`, `.ruff.toml`, or `pyrefly.toml`. Ruff, Pyrefly, and Biome load
186
+ their native project configurations in place of bundled defaults. Readability
187
+ does not interpret EditorConfig itself; a canonical tool such as Biome may opt
188
+ into it through that tool's native configuration. Gofmt has no project settings.
189
+ The bundled Biome file requires Biome 2.5 or later, matching the fallback
190
+ runner's version floor.
190
191
 
191
192
  ## Style Guides
192
193
 
@@ -338,14 +339,15 @@ readability sync
338
339
 
339
340
  ### Offline Mode
340
341
 
341
- The tool stores local copies of the style guides in the `guides/` directory and
342
+ The tool stores local copies of the style guides in the
343
+ `readability/guides/` directory and
342
344
  the `guide` command uses these local files when they exist. The bundled copies
343
345
  are automatically synchronized weekly from the official
344
346
  [Google Style Guides](https://google.github.io/styleguide/) repository via
345
347
  GitHub Actions, and you can refresh your local cache at any time with the `sync`
346
348
  command.
347
349
 
348
- You can override the default `guides/` directory by setting the
350
+ You can override the default guide directory by setting the
349
351
  `READABILITY_CACHE` environment variable. This is useful if you want to store
350
352
  the guides in a specific location or share them across different installations:
351
353
 
@@ -9,9 +9,9 @@ quick access to style conventions without browsing HTML pages.
9
9
  ## Features
10
10
 
11
11
  - **Linting & Formatting**: A `check` command that automatically detects and
12
- runs relevant tools (Ruff, Pyrefly, Biome, Prettier, gofmt) for your project.
13
- - **Sensible Defaults**: Google-style defaults for Ruff, Pyrefly, Biome, and
14
- Prettier are used automatically when a project does not define its own.
12
+ runs relevant tools (Ruff, Pyrefly, Biome, and gofmt) for your project.
13
+ - **Predictable Ownership**: Every supported format has fixed tools, so
14
+ configuration customizes checks without changing which formatter runs.
15
15
  - **Style Guides**: A `guide` command that fetches the latest Google style
16
16
  guides (Python, Shell, C++, Java, JS/TS, Go, etc.) converted to Markdown, and
17
17
  outlines, addresses, and searches them by section rather than serving 200 KB
@@ -64,8 +64,8 @@ uv run readability sync
64
64
  ## Checking and Formatting
65
65
 
66
66
  The `check` command identifies and runs relevant linting and formatting tools
67
- based on file extensions and the presence of configuration files (triggers) in
68
- your project root:
67
+ solely from file extensions. Configuration can customize the assigned tools,
68
+ but cannot opt another tool into a format:
69
69
 
70
70
  ```bash
71
71
  # Run checks on the current directory
@@ -78,21 +78,22 @@ readability check src/ tests/ main.py
78
78
  readability check . --fix
79
79
  ```
80
80
 
81
- ### Supported Tools
81
+ ### Supported Formats
82
82
 
83
- | Tool | Supported Extensions | Config Needed | Fetched By |
84
- | ------------ | --------------------------------------------------------------------------------------- | --------------------------------- | ---------- |
85
- | **Ruff** | `.py` | none, bundled | `uvx` |
86
- | **Pyrefly** | `.py` | none, bundled | `uvx` |
87
- | **Biome** | `.js`, `.ts`, `.jsx`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` | none, bundled | `npx` |
88
- | **Prettier** | `.js`, `.ts`, `.jsx`, `.tsx`, `.json`, `.css`, `.scss`, `.html`, `.md`, `.yml`, `.yaml` | none for Markdown, YAML, and SCSS | `npx` |
89
- | **gofmt** | `.go` | `go.mod` | — |
83
+ | Formats | Owners | Project configuration |
84
+ | ---------------------------------------------------------------- | --------------------------------------- | ------------------------------------- |
85
+ | `.py` | Ruff lint/format; Pyrefly type checking | Ruff and Pyrefly native configuration |
86
+ | `.js`, `.jsx`, `.ts`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` | Biome | `biome.json` or `biome.jsonc` |
87
+ | `.go` | gofmt | None |
90
88
 
91
- **You do not need to install any of these.** Each is resolved in order: a
92
- project-local install (`node_modules/.bin`, `.venv/bin`) first, then whatever is
93
- on your `PATH`, and only then fetched by `uvx` or `npx`. The runners cache what
94
- they download, so the fetch happens once per machine and later runs are served
95
- from disk and work offline.
89
+ Markdown, YAML, SCSS, JSONL, and extensions not listed above are unsupported.
90
+ An unsupported-only path reports that nothing was checked and exits
91
+ successfully.
92
+
93
+ Biome first checks a project's `node_modules/.bin`. Ruff, Pyrefly, and Biome
94
+ then use `PATH`, followed by `uvx` for the Python tools or `npx` for Biome. The
95
+ runners cache downloads, so subsequent runs work offline. Gofmt ships with Go
96
+ and must be available on `PATH`.
96
97
 
97
98
  That keeps this package at ~4 MB rather than the ~54 MB it would take to carry
98
99
  Ruff and Pyrefly itself — a cost that would fall on everyone using only `guide`.
@@ -109,18 +110,17 @@ over a fetched one.
109
110
  Set `UV_OFFLINE=1` to forbid fetching. A tool that then cannot be reached fails
110
111
  the run rather than passing it.
111
112
 
112
- Ruff, Pyrefly, and Biome ship with bundled configurations, so they run on any
113
- file they handle without the project arranging anything. Prettier does the same
114
- for Markdown, YAML, and SCSS; a Prettier configuration opts its overlapping web
115
- formats into Prettier too. Gofmt waits for a `go.mod` file.
113
+ Ruff, Pyrefly, and Biome ship with bundled configurations, so they run on every
114
+ file they own without project setup. Gofmt runs on `.go` files even when there
115
+ is no `go.mod`.
116
116
 
117
117
  A tool that could not be reached at all is never skipped quietly:
118
118
 
119
119
  ```bash
120
120
  # Some tools ran, so the result stands, but coverage was partial
121
121
  $ readability check src/
122
- Warning: not installed, so not run: prettier.
123
- No findings in 1 path(s) (ruff, pyrefly).
122
+ Warning: not installed, so not run: pyrefly.
123
+ No findings in 1 path(s) (ruff).
124
124
 
125
125
  # Nothing ran, so there is no result to report
126
126
  $ readability check src/
@@ -143,7 +143,7 @@ or exit-code decisions:
143
143
  ```python
144
144
  from pathlib import Path
145
145
 
146
- from readability import check_paths
146
+ from readability.checking import check_paths
147
147
 
148
148
  report = check_paths(["src", Path("tests")], project_root=Path.cwd())
149
149
  if report.findings or report.failed or not report.ran:
@@ -157,22 +157,23 @@ raises `FileNotFoundError` rather than being misreported as a finding. Relative
157
157
  paths remain relative to the process working directory; `project_root` controls
158
158
  configuration discovery only.
159
159
 
160
- ### Default Configurations
160
+ ### Configuring Formats
161
161
 
162
162
  Ruff and Pyrefly defaults follow the
163
163
  [Google Python style guide](https://google.github.io/styleguide/pyguide.html):
164
164
  80-column lines, Google docstrings, import ordering, and full type checking. The
165
165
  Biome default applies the 80-column lines and two-space indentation of the
166
166
  [Google JavaScript style guide](https://google.github.io/styleguide/jsguide.html)
167
- and enables Biome's recommended lint rules. Prettier wraps prose at 80 columns,
168
- following the
169
- [Google Markdown style guide](https://google.github.io/styleguide/docguide/style.html#character-line-limit).
170
- These defaults apply only when the project does not define its own
171
- configuration. To override them, add a `[tool.ruff]` or `[tool.pyrefly]` section
172
- to your `pyproject.toml`, or a dedicated `ruff.toml`, `pyrefly.toml`,
173
- `biome.json`, `biome.jsonc`, or Prettier configuration any project-level
174
- configuration takes full precedence. The bundled Biome file requires Biome 2.5
175
- or later, matching the version floor used by the fallback runner.
167
+ and enables Biome's recommended lint rules. Project `biome.json` and
168
+ `biome.jsonc` files replace those bundled defaults for Biome-owned formats.
169
+
170
+ For Python, add `[tool.ruff]` or `[tool.pyrefly]` to `pyproject.toml`, or use
171
+ `ruff.toml`, `.ruff.toml`, or `pyrefly.toml`. Ruff, Pyrefly, and Biome load
172
+ their native project configurations in place of bundled defaults. Readability
173
+ does not interpret EditorConfig itself; a canonical tool such as Biome may opt
174
+ into it through that tool's native configuration. Gofmt has no project settings.
175
+ The bundled Biome file requires Biome 2.5 or later, matching the fallback
176
+ runner's version floor.
176
177
 
177
178
  ## Style Guides
178
179
 
@@ -324,14 +325,15 @@ readability sync
324
325
 
325
326
  ### Offline Mode
326
327
 
327
- The tool stores local copies of the style guides in the `guides/` directory and
328
+ The tool stores local copies of the style guides in the
329
+ `readability/guides/` directory and
328
330
  the `guide` command uses these local files when they exist. The bundled copies
329
331
  are automatically synchronized weekly from the official
330
332
  [Google Style Guides](https://google.github.io/styleguide/) repository via
331
333
  GitHub Actions, and you can refresh your local cache at any time with the `sync`
332
334
  command.
333
335
 
334
- You can override the default `guides/` directory by setting the
336
+ You can override the default guide directory by setting the
335
337
  `READABILITY_CACHE` environment variable. This is useful if you want to store
336
338
  the guides in a specific location or share them across different installations:
337
339
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "readability-cli"
3
- version = "0.8.6"
3
+ version = "0.9.0"
4
4
  description = "A CLI to lint, format, and type-check code with Google-style defaults, and pull Google style guides in markdown format."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -16,17 +16,13 @@ Homepage = "https://github.com/owahltinez/readability"
16
16
  Repository = "https://github.com/owahltinez/readability"
17
17
 
18
18
  [project.scripts]
19
- readability = "readability:main"
19
+ readability = "readability.cli:main"
20
20
 
21
21
  [tool.uv]
22
22
  package = true
23
23
 
24
24
  [tool.hatch.build.targets.wheel]
25
- include = [
26
- "/readability.py",
27
- "/guides",
28
- "/configs",
29
- ]
25
+ packages = ["readability"]
30
26
 
31
27
  [build-system]
32
28
  requires = ["hatchling"]
@@ -54,3 +50,4 @@ convention = "google"
54
50
  [tool.ruff.lint.per-file-ignores]
55
51
  "test_*.py" = ["D"]
56
52
  "*_test.py" = ["D"]
53
+ "readability/__init__.py" = ["D104"]
File without changes
@@ -0,0 +1,265 @@
1
+ """Run readability checks and report their coverage."""
2
+
3
+ import dataclasses
4
+ import logging
5
+ import subprocess
6
+ from collections.abc import Sequence
7
+ from pathlib import Path
8
+
9
+ import click
10
+
11
+ from readability.tools import (
12
+ ToolPlan,
13
+ _command_batches,
14
+ _get_tool_definitions,
15
+ _should_run_tool,
16
+ _tool_is_installed,
17
+ )
18
+
19
+ logger = logging.getLogger("readability")
20
+ DEFAULT_TIMEOUT = 60
21
+
22
+
23
+ @dataclasses.dataclass
24
+ class CheckReport:
25
+ """What a check actually did, as opposed to what it was asked to do.
26
+
27
+ Attributes:
28
+ findings: Whether any tool that ran reported something.
29
+ ran: Names of the tools that processed at least one file. This records
30
+ what happened, not what was intended. A tool that resolved but
31
+ never started belongs in failed, and a tool that explicitly
32
+ reported processing no files is not recorded as having run.
33
+ skipped: Names of the tools that were applicable but not installed.
34
+ Tools that own no requested file are in neither set, so passing
35
+ without them is not a gap in coverage.
36
+ failed: Names of the tools that started and could not finish, by
37
+ failing to exec or by running past the timeout.
38
+ unverified_paths: Requested paths for which no tool processed a file.
39
+ Keeping these paths through aggregation prevents a checked path
40
+ from hiding another path that a tool ignored.
41
+ """
42
+
43
+ findings: bool = False
44
+ ran: set[str] = dataclasses.field(default_factory=set)
45
+ skipped: set[str] = dataclasses.field(default_factory=set)
46
+ failed: set[str] = dataclasses.field(default_factory=set)
47
+ unverified_paths: list[Path] = dataclasses.field(default_factory=list)
48
+
49
+ def absorb(self, other: "CheckReport") -> None:
50
+ """Fold another report into this one.
51
+
52
+ Args:
53
+ other: The report to merge, typically for one more path.
54
+ """
55
+ self.findings |= other.findings
56
+ self.ran |= other.ran
57
+ self.skipped |= other.skipped
58
+ self.failed |= other.failed
59
+ self.unverified_paths.extend(other.unverified_paths)
60
+
61
+
62
+ def check_paths(
63
+ paths: Sequence[str | Path],
64
+ project_root: Path | None = None,
65
+ fix: bool = False,
66
+ ) -> CheckReport:
67
+ """Run relevant checks for paths and aggregate what the tools did.
68
+
69
+ Detailed findings from the underlying tools are written as they are for
70
+ the command-line interface. This function does not print CLI status prose
71
+ or turn the report into a process exit code; callers decide how to handle
72
+ the result.
73
+
74
+ Args:
75
+ paths: Files or directories to check, as strings or paths. Relative
76
+ paths are interpreted from the current working directory.
77
+ project_root: Root used only to discover tool configuration. Defaults
78
+ to the current working directory; it does not rebase paths.
79
+ fix: Whether to apply automatic fixes.
80
+
81
+ Returns:
82
+ A report aggregated across all provided paths.
83
+
84
+ Raises:
85
+ FileNotFoundError: If any requested path does not exist. Every path is
86
+ validated before any tools run.
87
+ """
88
+ root = Path.cwd() if project_root is None else project_root
89
+ requested_paths = [Path(path) for path in paths]
90
+ missing_path = next(
91
+ (path for path in requested_paths if not path.exists()), None
92
+ )
93
+ if missing_path is not None:
94
+ raise FileNotFoundError(f"Path does not exist: {missing_path}")
95
+
96
+ report = CheckReport()
97
+ for path in requested_paths:
98
+ path_report = _check_path(path, root, fix=fix)
99
+ if not path_report.ran:
100
+ path_report.unverified_paths.append(path)
101
+ report.absorb(path_report)
102
+ return report
103
+
104
+
105
+ def _check_path(
106
+ path: Path, project_root: Path, fix: bool = False
107
+ ) -> CheckReport:
108
+ """Apply relevant tools to a single path.
109
+
110
+ Args:
111
+ path: The path (file or directory) to check.
112
+ project_root: The root used for native tool configuration discovery.
113
+ fix: Whether to apply automatic fixes.
114
+
115
+ Returns:
116
+ What the tools applicable to this path did.
117
+ """
118
+ logger.info("Checking path: %s", path)
119
+
120
+ # Iterate through all supported tool definitions
121
+ report = CheckReport()
122
+ for tool in _get_tool_definitions(path, project_root):
123
+ if not _should_run_tool(tool, path):
124
+ continue
125
+
126
+ # A tool that is wanted but absent leaves a hole in the coverage,
127
+ # which is not the same as a clean result
128
+ if not _tool_is_installed(tool):
129
+ report.skipped.add(tool.name)
130
+ continue
131
+
132
+ report.absorb(_run_tool(tool, fix=fix))
133
+
134
+ return report
135
+
136
+
137
+ def _run_tool(
138
+ tool: ToolPlan,
139
+ fix: bool = False,
140
+ ) -> CheckReport:
141
+ """Orchestrate the execution of a specific formatting or linting tool.
142
+
143
+ Args:
144
+ tool: The executable plan to run.
145
+ fix: Whether to apply automatic fixes.
146
+
147
+ Returns:
148
+ What the tool did: whether it reported findings, and whether it
149
+ completed at all. A tool that could not be started or ran past the
150
+ timeout verified nothing, and saying so is the difference between a
151
+ pass and a command that only looks like one.
152
+ """
153
+ logger.info("Running %s...", tool.name)
154
+
155
+ report = CheckReport()
156
+ target_count = len(tool.targets or ())
157
+ try:
158
+ if fix:
159
+ # Formatters rewrite files, fixers apply what they can. Both
160
+ # exit non-zero when something is left over, which is a finding
161
+ # rather than a failure, so the check below still gets to run
162
+ # and report what they could not deal with.
163
+ for phase in ("format", "fix"):
164
+ configured_command = getattr(tool, phase)
165
+ if configured_command:
166
+ commands = _command_batches(
167
+ configured_command, target_count
168
+ )
169
+ for command in commands:
170
+ result = _capture_tool_command(command, cwd=tool.cwd)
171
+ if _tool_checked_files(tool.name, result):
172
+ report.ran.add(tool.name)
173
+ if result.returncode != 0:
174
+ report.findings = True
175
+ finding_type = (
176
+ "formatting findings"
177
+ if phase == "format"
178
+ else "findings"
179
+ )
180
+ click.echo(
181
+ f"--- {tool.name} {finding_type} ---\n"
182
+ f"{result.stdout}\n{result.stderr}"
183
+ )
184
+ elif tool.check_format:
185
+ for command in _command_batches(tool.check_format, target_count):
186
+ result = _capture_tool_command(command, cwd=tool.cwd)
187
+ if _tool_checked_files(tool.name, result):
188
+ report.ran.add(tool.name)
189
+ # gofmt reports by naming files rather than by exit code
190
+ if result.returncode != 0 or (
191
+ tool.name == "gofmt" and result.stdout.strip()
192
+ ):
193
+ report.findings = True
194
+ click.echo(
195
+ f"--- {tool.name} formatting findings ---\n"
196
+ f"{result.stdout}\n{result.stderr}"
197
+ )
198
+
199
+ if tool.check:
200
+ for command in _command_batches(tool.check, target_count):
201
+ result = _capture_tool_command(command, cwd=tool.cwd)
202
+ if _tool_checked_files(tool.name, result):
203
+ report.ran.add(tool.name)
204
+ if result.returncode != 0:
205
+ report.findings = True
206
+ click.echo(
207
+ f"--- {tool.name} findings ---\n"
208
+ f"{result.stdout}\n{result.stderr}"
209
+ )
210
+
211
+ # Failing to start, or running past the timeout, means this tool checked
212
+ # nothing. Whatever it managed before that stays in ran.
213
+ except (subprocess.SubprocessError, OSError) as e:
214
+ logger.warning("Could not run %s: %s", tool.name, e)
215
+ report.failed.add(tool.name)
216
+
217
+ return report
218
+
219
+
220
+ def _tool_checked_files(
221
+ tool_name: str, result: subprocess.CompletedProcess
222
+ ) -> bool:
223
+ """Report whether a completed command actually processed any files.
224
+
225
+ Args:
226
+ tool_name: The tool whose command completed.
227
+ result: The completed subprocess.
228
+
229
+ Returns:
230
+ False when Biome explicitly reports that it checked zero files.
231
+ """
232
+ if tool_name != "biome":
233
+ return True
234
+ output = f"{result.stdout or ''}\n{result.stderr or ''}"
235
+ # Biome 2.x uses these summaries for an unmatched target.
236
+ zero_file_summaries = ("Checked 0 files", "Formatted 0 files")
237
+ return not any(summary in output for summary in zero_file_summaries)
238
+
239
+
240
+ def _capture_tool_command(
241
+ cmd: list[str], cwd: Path | None = None
242
+ ) -> subprocess.CompletedProcess:
243
+ """Run a tool and capture what it said, whatever its exit code.
244
+
245
+ Args:
246
+ cmd: The command list to execute.
247
+ cwd: Directory in which the tool should run. Defaults to the caller's
248
+ current working directory.
249
+
250
+ Returns:
251
+ The completed process, including captured output and its exit code.
252
+
253
+ Raises:
254
+ subprocess.SubprocessError: If the command outlives the timeout.
255
+ OSError: If the command cannot be started.
256
+ """
257
+ logger.debug("Executing: %s", " ".join(cmd))
258
+ return subprocess.run(
259
+ cmd,
260
+ capture_output=True,
261
+ text=True,
262
+ check=False,
263
+ timeout=DEFAULT_TIMEOUT,
264
+ cwd=cwd,
265
+ )