readability-cli 0.8.2__tar.gz → 0.8.3__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.
- {readability_cli-0.8.2 → readability_cli-0.8.3}/PKG-INFO +25 -17
- {readability_cli-0.8.2 → readability_cli-0.8.3}/README.md +24 -16
- readability_cli-0.8.3/configs/biome-default.json +14 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/pyproject.toml +1 -1
- {readability_cli-0.8.2 → readability_cli-0.8.3}/readability.py +95 -37
- {readability_cli-0.8.2 → readability_cli-0.8.3}/test_readability.py +254 -7
- {readability_cli-0.8.2 → readability_cli-0.8.3}/uv.lock +1 -1
- {readability_cli-0.8.2 → readability_cli-0.8.3}/.github/workflows/ci.yml +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/.github/workflows/publish.yml +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/.github/workflows/update-guides.yml +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/.gitignore +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/.python-version +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/LICENSE +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/configs/pyrefly.toml +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/configs/ruff.toml +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/guides/Rguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/guides/cppguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/guides/csharp-style.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/guides/docguide-style.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/guides/go-guide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/guides/htmlcssguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/guides/javaguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/guides/jsguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/guides/jsoncstyleguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/guides/objcguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/guides/pyguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/guides/shellguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/guides/tsguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.3}/guides/vimscriptguide.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: readability-cli
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.3
|
|
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
|
|
@@ -24,8 +24,9 @@ and quick access to style conventions without browsing HTML pages.
|
|
|
24
24
|
|
|
25
25
|
- **Linting & Formatting**: A `check` command that automatically detects and
|
|
26
26
|
runs relevant tools (Ruff, Pyrefly, Biome, Prettier, gofmt) for your project.
|
|
27
|
-
- **Sensible Defaults**: Bundled Google-style configurations for Ruff
|
|
28
|
-
Pyrefly are used automatically when a project does not define its
|
|
27
|
+
- **Sensible Defaults**: Bundled Google-style configurations for Ruff,
|
|
28
|
+
Pyrefly, and Biome are used automatically when a project does not define its
|
|
29
|
+
own.
|
|
29
30
|
- **Style Guides**: A `guide` command that fetches the latest Google style
|
|
30
31
|
guides (Python, Shell, C++, Java, JS/TS, Go, etc.) converted to Markdown,
|
|
31
32
|
and outlines, addresses, and searches them by section rather than serving
|
|
@@ -98,7 +99,7 @@ readability check . --fix
|
|
|
98
99
|
|------|----------------------|---------------|------------|
|
|
99
100
|
| **Ruff** | `.py` | none, bundled | `uvx` |
|
|
100
101
|
| **Pyrefly** | `.py` | none, bundled | `uvx` |
|
|
101
|
-
| **Biome** | `.js`, `.ts`, `.jsx`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` |
|
|
102
|
+
| **Biome** | `.js`, `.ts`, `.jsx`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` | none, bundled | `npx` |
|
|
102
103
|
| **Prettier** | `.js`, `.ts`, `.jsx`, `.tsx`, `.json`, `.css`, `.scss`, `.html`, `.md`, `.yml`, `.yaml` | `.prettierrc*` or `prettier.config.*` | `npx` |
|
|
103
104
|
| **gofmt** | `.go` | `go.mod` | — |
|
|
104
105
|
|
|
@@ -123,10 +124,9 @@ always wins over a fetched one.
|
|
|
123
124
|
Set `UV_OFFLINE=1` to forbid fetching. A tool that then cannot be reached
|
|
124
125
|
fails the run rather than passing it.
|
|
125
126
|
|
|
126
|
-
Ruff and
|
|
127
|
-
they handle without the project arranging anything.
|
|
128
|
-
bring no defaults here, so they wait until the project asks with a config
|
|
129
|
-
file.
|
|
127
|
+
Ruff, Pyrefly, and Biome ship with bundled configurations, so they run on any
|
|
128
|
+
file they handle without the project arranging anything. Prettier and gofmt
|
|
129
|
+
bring no defaults here, so they wait until the project asks with a config file.
|
|
130
130
|
|
|
131
131
|
A tool that could not be reached at all is never skipped quietly:
|
|
132
132
|
|
|
@@ -144,8 +144,10 @@ Error: Every tool for 1 path(s) is missing, so nothing was verified.
|
|
|
144
144
|
That second case needs neither the tool nor a runner to be present, which is
|
|
145
145
|
the state a container image is usually in.
|
|
146
146
|
|
|
147
|
-
`check` exits non-zero on findings
|
|
148
|
-
gate CI without a clean exit
|
|
147
|
+
`check` exits non-zero on findings, tool failures, and when every applicable
|
|
148
|
+
tool is absent, so it can gate CI without a clean exit meaning required tools
|
|
149
|
+
were missing. If no tool applies to the requested paths, it reports that fact
|
|
150
|
+
and exits successfully.
|
|
149
151
|
|
|
150
152
|
### Python API
|
|
151
153
|
|
|
@@ -171,13 +173,19 @@ Relative paths remain relative to the process working directory;
|
|
|
171
173
|
|
|
172
174
|
### Default Configurations
|
|
173
175
|
|
|
174
|
-
|
|
175
|
-
[Google Python style guide](https://google.github.io/styleguide/pyguide.html)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
project
|
|
176
|
+
Ruff and Pyrefly defaults follow the
|
|
177
|
+
[Google Python style guide](https://google.github.io/styleguide/pyguide.html):
|
|
178
|
+
80-column lines, Google docstrings, import ordering, and full type checking.
|
|
179
|
+
The Biome default applies the 80-column lines and two-space indentation of the
|
|
180
|
+
[Google JavaScript style guide](https://google.github.io/styleguide/jsguide.html)
|
|
181
|
+
and enables Biome's recommended lint rules. These defaults apply only when the
|
|
182
|
+
project does not define its own configuration. To override them, add a
|
|
183
|
+
`[tool.ruff]` or `[tool.pyrefly]` section to your `pyproject.toml`, or a
|
|
184
|
+
dedicated `ruff.toml`, `pyrefly.toml`, `biome.json`, or `biome.jsonc` — any
|
|
185
|
+
project-level configuration takes full precedence. The bundled Biome file
|
|
186
|
+
intentionally uses only settings supported by both Biome 1.x and 2.x, so a
|
|
187
|
+
project-local installation remains authoritative without creating a schema
|
|
188
|
+
mismatch.
|
|
181
189
|
|
|
182
190
|
## Style Guides
|
|
183
191
|
|
|
@@ -10,8 +10,9 @@ and quick access to style conventions without browsing HTML pages.
|
|
|
10
10
|
|
|
11
11
|
- **Linting & Formatting**: A `check` command that automatically detects and
|
|
12
12
|
runs relevant tools (Ruff, Pyrefly, Biome, Prettier, gofmt) for your project.
|
|
13
|
-
- **Sensible Defaults**: Bundled Google-style configurations for Ruff
|
|
14
|
-
Pyrefly are used automatically when a project does not define its
|
|
13
|
+
- **Sensible Defaults**: Bundled Google-style configurations for Ruff,
|
|
14
|
+
Pyrefly, and Biome are used automatically when a project does not define its
|
|
15
|
+
own.
|
|
15
16
|
- **Style Guides**: A `guide` command that fetches the latest Google style
|
|
16
17
|
guides (Python, Shell, C++, Java, JS/TS, Go, etc.) converted to Markdown,
|
|
17
18
|
and outlines, addresses, and searches them by section rather than serving
|
|
@@ -84,7 +85,7 @@ readability check . --fix
|
|
|
84
85
|
|------|----------------------|---------------|------------|
|
|
85
86
|
| **Ruff** | `.py` | none, bundled | `uvx` |
|
|
86
87
|
| **Pyrefly** | `.py` | none, bundled | `uvx` |
|
|
87
|
-
| **Biome** | `.js`, `.ts`, `.jsx`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` |
|
|
88
|
+
| **Biome** | `.js`, `.ts`, `.jsx`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` | none, bundled | `npx` |
|
|
88
89
|
| **Prettier** | `.js`, `.ts`, `.jsx`, `.tsx`, `.json`, `.css`, `.scss`, `.html`, `.md`, `.yml`, `.yaml` | `.prettierrc*` or `prettier.config.*` | `npx` |
|
|
89
90
|
| **gofmt** | `.go` | `go.mod` | — |
|
|
90
91
|
|
|
@@ -109,10 +110,9 @@ always wins over a fetched one.
|
|
|
109
110
|
Set `UV_OFFLINE=1` to forbid fetching. A tool that then cannot be reached
|
|
110
111
|
fails the run rather than passing it.
|
|
111
112
|
|
|
112
|
-
Ruff and
|
|
113
|
-
they handle without the project arranging anything.
|
|
114
|
-
bring no defaults here, so they wait until the project asks with a config
|
|
115
|
-
file.
|
|
113
|
+
Ruff, Pyrefly, and Biome ship with bundled configurations, so they run on any
|
|
114
|
+
file they handle without the project arranging anything. Prettier and gofmt
|
|
115
|
+
bring no defaults here, so they wait until the project asks with a config file.
|
|
116
116
|
|
|
117
117
|
A tool that could not be reached at all is never skipped quietly:
|
|
118
118
|
|
|
@@ -130,8 +130,10 @@ Error: Every tool for 1 path(s) is missing, so nothing was verified.
|
|
|
130
130
|
That second case needs neither the tool nor a runner to be present, which is
|
|
131
131
|
the state a container image is usually in.
|
|
132
132
|
|
|
133
|
-
`check` exits non-zero on findings
|
|
134
|
-
gate CI without a clean exit
|
|
133
|
+
`check` exits non-zero on findings, tool failures, and when every applicable
|
|
134
|
+
tool is absent, so it can gate CI without a clean exit meaning required tools
|
|
135
|
+
were missing. If no tool applies to the requested paths, it reports that fact
|
|
136
|
+
and exits successfully.
|
|
135
137
|
|
|
136
138
|
### Python API
|
|
137
139
|
|
|
@@ -157,13 +159,19 @@ Relative paths remain relative to the process working directory;
|
|
|
157
159
|
|
|
158
160
|
### Default Configurations
|
|
159
161
|
|
|
160
|
-
|
|
161
|
-
[Google Python style guide](https://google.github.io/styleguide/pyguide.html)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
project
|
|
162
|
+
Ruff and Pyrefly defaults follow the
|
|
163
|
+
[Google Python style guide](https://google.github.io/styleguide/pyguide.html):
|
|
164
|
+
80-column lines, Google docstrings, import ordering, and full type checking.
|
|
165
|
+
The Biome default applies the 80-column lines and two-space indentation of the
|
|
166
|
+
[Google JavaScript style guide](https://google.github.io/styleguide/jsguide.html)
|
|
167
|
+
and enables Biome's recommended lint rules. These defaults apply only when the
|
|
168
|
+
project does not define its own configuration. To override them, add a
|
|
169
|
+
`[tool.ruff]` or `[tool.pyrefly]` section to your `pyproject.toml`, or a
|
|
170
|
+
dedicated `ruff.toml`, `pyrefly.toml`, `biome.json`, or `biome.jsonc` — any
|
|
171
|
+
project-level configuration takes full precedence. The bundled Biome file
|
|
172
|
+
intentionally uses only settings supported by both Biome 1.x and 2.x, so a
|
|
173
|
+
project-local installation remains authoritative without creating a schema
|
|
174
|
+
mismatch.
|
|
167
175
|
|
|
168
176
|
## Style Guides
|
|
169
177
|
|
|
@@ -1116,6 +1116,11 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
|
|
|
1116
1116
|
)
|
|
1117
1117
|
sys.exit(1)
|
|
1118
1118
|
|
|
1119
|
+
# Findings remain a failed check even when a tool reports that it could
|
|
1120
|
+
# not process any files (for example, a Biome configuration error).
|
|
1121
|
+
if report.findings:
|
|
1122
|
+
sys.exit(1)
|
|
1123
|
+
|
|
1119
1124
|
# Having run nothing is not a pass. Reporting it as one is how this
|
|
1120
1125
|
# command became a silent no-op wherever its tools were absent, gating
|
|
1121
1126
|
# nothing while every caller read the exit code as approval.
|
|
@@ -1137,13 +1142,19 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
|
|
|
1137
1142
|
)
|
|
1138
1143
|
return
|
|
1139
1144
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1145
|
+
checked_path_count = len(paths) - len(report.unverified_paths)
|
|
1146
|
+
if report.unverified_paths:
|
|
1147
|
+
click.echo(
|
|
1148
|
+
f"Warning: nothing was checked for "
|
|
1149
|
+
f"{len(report.unverified_paths)} path(s): "
|
|
1150
|
+
f"{', '.join(map(str, report.unverified_paths))}.",
|
|
1151
|
+
err=True,
|
|
1152
|
+
)
|
|
1142
1153
|
|
|
1143
1154
|
# Findings are the only thing this command printed, so a clean run said
|
|
1144
1155
|
# nothing at all and left the caller unable to tell it from a no-op.
|
|
1145
1156
|
click.echo(
|
|
1146
|
-
f"No findings in {
|
|
1157
|
+
f"No findings in {checked_path_count} path(s) "
|
|
1147
1158
|
f"({', '.join(sorted(report.ran))}).",
|
|
1148
1159
|
err=True,
|
|
1149
1160
|
)
|
|
@@ -1155,21 +1166,25 @@ class CheckReport:
|
|
|
1155
1166
|
|
|
1156
1167
|
Attributes:
|
|
1157
1168
|
findings: Whether any tool that ran reported something.
|
|
1158
|
-
ran: Names of the tools that
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1169
|
+
ran: Names of the tools that processed at least one file. This records
|
|
1170
|
+
what happened, not what was intended. A tool that resolved but
|
|
1171
|
+
never started belongs in failed, and a tool that explicitly
|
|
1172
|
+
reported processing no files is not recorded as having run.
|
|
1162
1173
|
skipped: Names of the tools that were applicable but not installed.
|
|
1163
1174
|
Tools with no trigger file are in neither set: they were never
|
|
1164
1175
|
wanted, so passing without them is not a gap in coverage.
|
|
1165
1176
|
failed: Names of the tools that started and could not finish, by
|
|
1166
1177
|
failing to exec or by running past the timeout.
|
|
1178
|
+
unverified_paths: Requested paths for which no tool processed a file.
|
|
1179
|
+
Keeping these paths through aggregation prevents a checked path
|
|
1180
|
+
from hiding another path that a tool ignored.
|
|
1167
1181
|
"""
|
|
1168
1182
|
|
|
1169
1183
|
findings: bool = False
|
|
1170
1184
|
ran: set[str] = dataclasses.field(default_factory=set)
|
|
1171
1185
|
skipped: set[str] = dataclasses.field(default_factory=set)
|
|
1172
1186
|
failed: set[str] = dataclasses.field(default_factory=set)
|
|
1187
|
+
unverified_paths: list[Path] = dataclasses.field(default_factory=list)
|
|
1173
1188
|
|
|
1174
1189
|
def absorb(self, other: "CheckReport") -> None:
|
|
1175
1190
|
"""Fold another report into this one.
|
|
@@ -1181,6 +1196,7 @@ class CheckReport:
|
|
|
1181
1196
|
self.ran |= other.ran
|
|
1182
1197
|
self.skipped |= other.skipped
|
|
1183
1198
|
self.failed |= other.failed
|
|
1199
|
+
self.unverified_paths.extend(other.unverified_paths)
|
|
1184
1200
|
|
|
1185
1201
|
|
|
1186
1202
|
def check_paths(
|
|
@@ -1219,7 +1235,10 @@ def check_paths(
|
|
|
1219
1235
|
|
|
1220
1236
|
report = CheckReport()
|
|
1221
1237
|
for path in requested_paths:
|
|
1222
|
-
|
|
1238
|
+
path_report = _check_path(path, root, fix=fix)
|
|
1239
|
+
if not path_report.ran:
|
|
1240
|
+
path_report.unverified_paths.append(path)
|
|
1241
|
+
report.absorb(path_report)
|
|
1223
1242
|
return report
|
|
1224
1243
|
|
|
1225
1244
|
|
|
@@ -1278,12 +1297,20 @@ def _should_run_tool(
|
|
|
1278
1297
|
(project_root / t).exists() for t in tool["trigger"]
|
|
1279
1298
|
)
|
|
1280
1299
|
|
|
1281
|
-
#
|
|
1300
|
+
# Files must match one of the tool's supported extensions.
|
|
1282
1301
|
if path.is_file():
|
|
1283
1302
|
return has_trigger and path.suffix in tool["extensions"]
|
|
1284
1303
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1304
|
+
if not has_trigger:
|
|
1305
|
+
return False
|
|
1306
|
+
|
|
1307
|
+
# A directory trigger alone is not evidence that the tool checked a file.
|
|
1308
|
+
# Several tools deliberately exit zero when nothing matches, which would
|
|
1309
|
+
# otherwise turn an unsupported-only tree into a reported clean result.
|
|
1310
|
+
return any(
|
|
1311
|
+
candidate.is_file() and candidate.suffix in tool["extensions"]
|
|
1312
|
+
for candidate in path.rglob("*")
|
|
1313
|
+
)
|
|
1287
1314
|
|
|
1288
1315
|
|
|
1289
1316
|
def _tool_is_installed(tool_name: str, tool_config: dict[str, Any]) -> bool:
|
|
@@ -1319,12 +1346,15 @@ def _bundled_config(tool_name: str) -> Path:
|
|
|
1319
1346
|
"""Get the path to the bundled default configuration for a tool.
|
|
1320
1347
|
|
|
1321
1348
|
Args:
|
|
1322
|
-
tool_name: The name of the tool (e.g. "ruff", "pyrefly").
|
|
1349
|
+
tool_name: The name of the tool (e.g. "ruff", "pyrefly", "biome").
|
|
1323
1350
|
|
|
1324
1351
|
Returns:
|
|
1325
1352
|
The path to the bundled default config file.
|
|
1326
1353
|
"""
|
|
1327
|
-
|
|
1354
|
+
filename = (
|
|
1355
|
+
"biome-default.json" if tool_name == "biome" else f"{tool_name}.toml"
|
|
1356
|
+
)
|
|
1357
|
+
return Path(__file__).parent / "configs" / filename
|
|
1328
1358
|
|
|
1329
1359
|
|
|
1330
1360
|
def _has_project_config(
|
|
@@ -1456,6 +1486,26 @@ def _get_tool_definitions(
|
|
|
1456
1486
|
pyrefly_config = _default_config_args(
|
|
1457
1487
|
project_root, ["pyrefly.toml"], "pyrefly"
|
|
1458
1488
|
)
|
|
1489
|
+
biome_config = []
|
|
1490
|
+
if not any(
|
|
1491
|
+
(project_root / filename).exists()
|
|
1492
|
+
for filename in ("biome.json", "biome.jsonc")
|
|
1493
|
+
):
|
|
1494
|
+
biome_config = ["--config-path", str(_bundled_config("biome"))]
|
|
1495
|
+
ignore_files = (
|
|
1496
|
+
project_root / ".gitignore",
|
|
1497
|
+
project_root / ".ignore",
|
|
1498
|
+
project_root / ".git" / "info" / "exclude",
|
|
1499
|
+
)
|
|
1500
|
+
if any(ignore_file.is_file() for ignore_file in ignore_files):
|
|
1501
|
+
biome_config.extend(
|
|
1502
|
+
[
|
|
1503
|
+
"--vcs-enabled=true",
|
|
1504
|
+
"--vcs-client-kind=git",
|
|
1505
|
+
"--vcs-use-ignore-file=true",
|
|
1506
|
+
f"--vcs-root={project_root}",
|
|
1507
|
+
]
|
|
1508
|
+
)
|
|
1459
1509
|
|
|
1460
1510
|
# Resolved once so every command for a tool reaches the same executable
|
|
1461
1511
|
ruff = _tool_command("ruff", project_root)
|
|
@@ -1511,12 +1561,14 @@ def _get_tool_definitions(
|
|
|
1511
1561
|
"check": [
|
|
1512
1562
|
*biome,
|
|
1513
1563
|
"lint",
|
|
1564
|
+
*biome_config,
|
|
1514
1565
|
"--no-errors-on-unmatched",
|
|
1515
1566
|
path_str,
|
|
1516
1567
|
],
|
|
1517
1568
|
"check_format": [
|
|
1518
1569
|
*biome,
|
|
1519
1570
|
"format",
|
|
1571
|
+
*biome_config,
|
|
1520
1572
|
"--no-errors-on-unmatched",
|
|
1521
1573
|
path_str,
|
|
1522
1574
|
],
|
|
@@ -1524,6 +1576,7 @@ def _get_tool_definitions(
|
|
|
1524
1576
|
*biome,
|
|
1525
1577
|
"lint",
|
|
1526
1578
|
"--write",
|
|
1579
|
+
*biome_config,
|
|
1527
1580
|
"--no-errors-on-unmatched",
|
|
1528
1581
|
path_str,
|
|
1529
1582
|
],
|
|
@@ -1531,6 +1584,7 @@ def _get_tool_definitions(
|
|
|
1531
1584
|
*biome,
|
|
1532
1585
|
"format",
|
|
1533
1586
|
"--write",
|
|
1587
|
+
*biome_config,
|
|
1534
1588
|
"--no-errors-on-unmatched",
|
|
1535
1589
|
path_str,
|
|
1536
1590
|
],
|
|
@@ -1622,11 +1676,13 @@ def _run_tool(
|
|
|
1622
1676
|
# and report what they could not deal with.
|
|
1623
1677
|
for phase in ("format", "fix"):
|
|
1624
1678
|
if phase in tool_config:
|
|
1625
|
-
|
|
1626
|
-
|
|
1679
|
+
result = _capture_tool_command(tool_config[phase])
|
|
1680
|
+
if _tool_checked_files(tool_name, result):
|
|
1681
|
+
report.ran.add(tool_name)
|
|
1627
1682
|
elif "check_format" in tool_config:
|
|
1628
1683
|
result = _capture_tool_command(tool_config["check_format"])
|
|
1629
|
-
|
|
1684
|
+
if _tool_checked_files(tool_name, result):
|
|
1685
|
+
report.ran.add(tool_name)
|
|
1630
1686
|
# gofmt reports by naming files rather than by exit code
|
|
1631
1687
|
if result.returncode != 0 or (
|
|
1632
1688
|
tool_name == "go fmt" and result.stdout.strip()
|
|
@@ -1639,7 +1695,8 @@ def _run_tool(
|
|
|
1639
1695
|
|
|
1640
1696
|
if "check" in tool_config:
|
|
1641
1697
|
result = _capture_tool_command(tool_config["check"])
|
|
1642
|
-
|
|
1698
|
+
if _tool_checked_files(tool_name, result):
|
|
1699
|
+
report.ran.add(tool_name)
|
|
1643
1700
|
if result.returncode != 0:
|
|
1644
1701
|
report.findings = True
|
|
1645
1702
|
click.echo(
|
|
@@ -1656,6 +1713,26 @@ def _run_tool(
|
|
|
1656
1713
|
return report
|
|
1657
1714
|
|
|
1658
1715
|
|
|
1716
|
+
def _tool_checked_files(
|
|
1717
|
+
tool_name: str, result: subprocess.CompletedProcess
|
|
1718
|
+
) -> bool:
|
|
1719
|
+
"""Report whether a completed command actually processed any files.
|
|
1720
|
+
|
|
1721
|
+
Args:
|
|
1722
|
+
tool_name: The tool whose command completed.
|
|
1723
|
+
result: The completed subprocess.
|
|
1724
|
+
|
|
1725
|
+
Returns:
|
|
1726
|
+
False when Biome explicitly reports that it checked zero files.
|
|
1727
|
+
"""
|
|
1728
|
+
if tool_name != "biome":
|
|
1729
|
+
return True
|
|
1730
|
+
output = f"{result.stdout or ''}\n{result.stderr or ''}"
|
|
1731
|
+
# Biome 1.x and 2.x use these summaries for an unmatched target.
|
|
1732
|
+
zero_file_summaries = ("Checked 0 files", "Formatted 0 files")
|
|
1733
|
+
return not any(summary in output for summary in zero_file_summaries)
|
|
1734
|
+
|
|
1735
|
+
|
|
1659
1736
|
def _capture_tool_command(cmd: list[str]) -> subprocess.CompletedProcess:
|
|
1660
1737
|
"""Run a tool and capture what it said, whatever its exit code.
|
|
1661
1738
|
|
|
@@ -1663,7 +1740,7 @@ def _capture_tool_command(cmd: list[str]) -> subprocess.CompletedProcess:
|
|
|
1663
1740
|
cmd: The command list to execute.
|
|
1664
1741
|
|
|
1665
1742
|
Returns:
|
|
1666
|
-
The completed process,
|
|
1743
|
+
The completed process, including captured output and its exit code.
|
|
1667
1744
|
|
|
1668
1745
|
Raises:
|
|
1669
1746
|
subprocess.SubprocessError: If the command outlives the timeout.
|
|
@@ -1679,25 +1756,6 @@ def _capture_tool_command(cmd: list[str]) -> subprocess.CompletedProcess:
|
|
|
1679
1756
|
)
|
|
1680
1757
|
|
|
1681
1758
|
|
|
1682
|
-
def _execute_tool_command(cmd: list[str]) -> None:
|
|
1683
|
-
"""Execute a tool command, raising if it exits with a non-zero code.
|
|
1684
|
-
|
|
1685
|
-
Args:
|
|
1686
|
-
cmd: The command list to execute.
|
|
1687
|
-
|
|
1688
|
-
Raises:
|
|
1689
|
-
subprocess.SubprocessError: If the command outlives the timeout.
|
|
1690
|
-
OSError: If the command cannot be started.
|
|
1691
|
-
"""
|
|
1692
|
-
logger.debug("Executing: %s", " ".join(cmd))
|
|
1693
|
-
# check=False: a fixer exits non-zero when findings are left over, and
|
|
1694
|
-
# treating that as an error aborted the run before the check that
|
|
1695
|
-
# reports them, so --fix passed on files the plain check failed.
|
|
1696
|
-
subprocess.run(
|
|
1697
|
-
cmd, capture_output=True, check=False, timeout=DEFAULT_TIMEOUT
|
|
1698
|
-
)
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
1759
|
# Main entry point for the CLI
|
|
1702
1760
|
def main() -> None:
|
|
1703
1761
|
"""Main entry point for the CLI."""
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import logging
|
|
2
3
|
import os
|
|
3
4
|
import subprocess
|
|
@@ -505,15 +506,72 @@ def test_default_configs_omitted_when_project_configured(
|
|
|
505
506
|
assert "--config" not in tools["ruff"]["format"]
|
|
506
507
|
assert "--config" not in tools["pyrefly"]["check"]
|
|
507
508
|
|
|
509
|
+
ts_file = tmp_path / "script.ts"
|
|
510
|
+
ts_file.touch()
|
|
511
|
+
(tmp_path / "biome.json").write_text("{}")
|
|
512
|
+
tools = {t["name"]: t for t in _get_tool_definitions(ts_file, tmp_path)}
|
|
513
|
+
for command in ("check", "check_format", "fix", "format"):
|
|
514
|
+
assert "--config-path" not in tools["biome"][command]
|
|
508
515
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
516
|
+
|
|
517
|
+
def test_biome_bundled_config_is_injected_into_every_command(
|
|
518
|
+
tmp_path: Path,
|
|
519
|
+
) -> None:
|
|
520
|
+
"""Linting, formatting, and their fix forms share the safe default."""
|
|
521
|
+
ts_file = tmp_path / "script.ts"
|
|
522
|
+
ts_file.touch()
|
|
523
|
+
|
|
524
|
+
tools = {t["name"]: t for t in _get_tool_definitions(ts_file, tmp_path)}
|
|
525
|
+
config_path = str(_bundled_config("biome"))
|
|
526
|
+
|
|
527
|
+
for command_name in ("check", "check_format", "fix", "format"):
|
|
528
|
+
command = tools["biome"][command_name]
|
|
529
|
+
assert command[command.index("--config-path") + 1] == config_path
|
|
530
|
+
assert "--vcs-enabled=true" not in command
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
@pytest.mark.parametrize(
|
|
534
|
+
"ignore_file", (".gitignore", ".ignore", ".git/info/exclude")
|
|
535
|
+
)
|
|
536
|
+
def test_biome_bundled_config_respects_project_ignore_files(
|
|
537
|
+
tmp_path: Path, ignore_file: str
|
|
538
|
+
) -> None:
|
|
539
|
+
"""An external default keeps ignore discovery rooted in the project."""
|
|
540
|
+
ts_file = tmp_path / "script.ts"
|
|
541
|
+
ts_file.touch()
|
|
542
|
+
ignore_path = tmp_path / ignore_file
|
|
543
|
+
ignore_path.parent.mkdir(parents=True, exist_ok=True)
|
|
544
|
+
ignore_path.write_text("node_modules/\n")
|
|
545
|
+
|
|
546
|
+
tools = {t["name"]: t for t in _get_tool_definitions(ts_file, tmp_path)}
|
|
547
|
+
|
|
548
|
+
for command_name in ("check", "check_format", "fix", "format"):
|
|
549
|
+
command = tools["biome"][command_name]
|
|
550
|
+
assert "--vcs-enabled=true" in command
|
|
551
|
+
assert "--vcs-client-kind=git" in command
|
|
552
|
+
assert "--vcs-use-ignore-file=true" in command
|
|
553
|
+
assert f"--vcs-root={tmp_path}" in command
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def test_bundled_default_configs_are_valid() -> None:
|
|
557
|
+
"""Tests that the bundled default configs exist and parse."""
|
|
558
|
+
# Both Python configs must exist and be valid TOML
|
|
512
559
|
for tool in ("ruff", "pyrefly"):
|
|
513
560
|
config_path = _bundled_config(tool)
|
|
514
561
|
assert config_path.exists()
|
|
515
562
|
tomllib.loads(config_path.read_text())
|
|
516
563
|
|
|
564
|
+
biome_config_path = _bundled_config("biome")
|
|
565
|
+
assert biome_config_path.exists()
|
|
566
|
+
biome_config = json.loads(biome_config_path.read_text())
|
|
567
|
+
assert biome_config["formatter"] == {
|
|
568
|
+
"enabled": True,
|
|
569
|
+
"indentStyle": "space",
|
|
570
|
+
"indentWidth": 2,
|
|
571
|
+
"lineWidth": 80,
|
|
572
|
+
}
|
|
573
|
+
assert biome_config["linter"]["rules"]["recommended"] is True
|
|
574
|
+
|
|
517
575
|
# The ruff defaults follow the Google Python style guide
|
|
518
576
|
ruff_config = tomllib.loads(_bundled_config("ruff").read_text())
|
|
519
577
|
assert ruff_config["line-length"] == 80
|
|
@@ -560,6 +618,29 @@ def test_check_paths_aggregates_str_and_path_inputs(
|
|
|
560
618
|
]
|
|
561
619
|
|
|
562
620
|
|
|
621
|
+
@patch("readability._check_path")
|
|
622
|
+
def test_check_paths_preserves_unverified_paths_when_another_path_ran(
|
|
623
|
+
mock_check_path: MagicMock,
|
|
624
|
+
tmp_path: Path,
|
|
625
|
+
) -> None:
|
|
626
|
+
"""One checked path must not hide another path that processed no files."""
|
|
627
|
+
checked = tmp_path / "src"
|
|
628
|
+
unchecked = tmp_path / "ignored"
|
|
629
|
+
checked.mkdir()
|
|
630
|
+
unchecked.mkdir()
|
|
631
|
+
mock_check_path.side_effect = [
|
|
632
|
+
CheckReport(ran={"biome"}),
|
|
633
|
+
CheckReport(),
|
|
634
|
+
]
|
|
635
|
+
|
|
636
|
+
report = check_paths([checked, unchecked], project_root=tmp_path)
|
|
637
|
+
|
|
638
|
+
assert report == CheckReport(
|
|
639
|
+
ran={"biome"},
|
|
640
|
+
unverified_paths=[unchecked],
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
|
|
563
644
|
@patch("readability._check_path", return_value=CheckReport(ran={"ruff"}))
|
|
564
645
|
def test_check_paths_defaults_project_root_to_cwd(
|
|
565
646
|
mock_check_path: MagicMock,
|
|
@@ -1789,10 +1870,10 @@ def test_check_runs_bundled_default_tools_without_a_trigger(
|
|
|
1789
1870
|
|
|
1790
1871
|
@patch("shutil.which")
|
|
1791
1872
|
@patch("subprocess.run")
|
|
1792
|
-
def
|
|
1873
|
+
def test_check_runs_biome_without_a_project_config(
|
|
1793
1874
|
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1794
1875
|
) -> None:
|
|
1795
|
-
"""
|
|
1876
|
+
"""A bundled Biome config covers web files in unconfigured projects."""
|
|
1796
1877
|
mock_which.side_effect = lambda x: x
|
|
1797
1878
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
1798
1879
|
|
|
@@ -1803,13 +1884,179 @@ def test_check_still_gates_tools_without_bundled_defaults(
|
|
|
1803
1884
|
result = runner.invoke(cli, ["check", "script.ts"])
|
|
1804
1885
|
|
|
1805
1886
|
assert result.exit_code == 0
|
|
1887
|
+
called = [call.args[0] for call in mock_run.call_args_list]
|
|
1888
|
+
assert any(command[0] == "biome" for command in called)
|
|
1889
|
+
assert all("prettier" not in command for command in called)
|
|
1890
|
+
config_path = str(_bundled_config("biome"))
|
|
1891
|
+
biome_commands = [command for command in called if command[0] == "biome"]
|
|
1892
|
+
assert biome_commands
|
|
1893
|
+
assert all(
|
|
1894
|
+
command[command.index("--config-path") + 1] == config_path
|
|
1895
|
+
for command in biome_commands
|
|
1896
|
+
)
|
|
1897
|
+
|
|
1898
|
+
|
|
1899
|
+
@patch("shutil.which")
|
|
1900
|
+
@patch("subprocess.run")
|
|
1901
|
+
def test_bundled_tools_do_not_claim_an_unsupported_directory(
|
|
1902
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1903
|
+
) -> None:
|
|
1904
|
+
"""A zero-file tool run must not turn nothing checked into a clean pass."""
|
|
1905
|
+
mock_which.side_effect = lambda x: x
|
|
1906
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
1907
|
+
|
|
1908
|
+
runner = CliRunner()
|
|
1909
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1910
|
+
Path("notes.md").touch()
|
|
1911
|
+
|
|
1912
|
+
result = runner.invoke(cli, ["check", "."])
|
|
1913
|
+
|
|
1914
|
+
assert result.exit_code == 0
|
|
1915
|
+
output = result.stdout + result.stderr
|
|
1916
|
+
assert "nothing was checked" in output
|
|
1917
|
+
assert "No findings" not in output
|
|
1918
|
+
mock_run.assert_not_called()
|
|
1919
|
+
|
|
1920
|
+
|
|
1921
|
+
@patch("shutil.which")
|
|
1922
|
+
@patch("subprocess.run")
|
|
1923
|
+
def test_biome_zero_file_result_is_not_reported_as_clean(
|
|
1924
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1925
|
+
) -> None:
|
|
1926
|
+
"""Ignored matches must not make an unverified directory look clean."""
|
|
1927
|
+
mock_which.side_effect = lambda x: x if x == "biome" else None
|
|
1928
|
+
checked_nothing = MagicMock(
|
|
1929
|
+
returncode=0,
|
|
1930
|
+
stdout="",
|
|
1931
|
+
stderr="Checked 0 files in 1ms. No fixes applied.\n",
|
|
1932
|
+
)
|
|
1933
|
+
formatted_nothing = MagicMock(
|
|
1934
|
+
returncode=0,
|
|
1935
|
+
stdout="Formatted 0 files in 1ms. No fixes applied.\n",
|
|
1936
|
+
stderr="",
|
|
1937
|
+
)
|
|
1938
|
+
|
|
1939
|
+
runner = CliRunner()
|
|
1940
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1941
|
+
Path(".gitignore").write_text("node_modules/\n")
|
|
1942
|
+
Path("node_modules/example").mkdir(parents=True)
|
|
1943
|
+
Path("node_modules/example/index.js").touch()
|
|
1944
|
+
Path("README.md").touch()
|
|
1945
|
+
|
|
1946
|
+
for extra_args, expected_calls in (([], 2), (["--fix"], 3)):
|
|
1947
|
+
mock_run.side_effect = (
|
|
1948
|
+
[checked_nothing, checked_nothing]
|
|
1949
|
+
if not extra_args
|
|
1950
|
+
else [formatted_nothing, checked_nothing, checked_nothing]
|
|
1951
|
+
)
|
|
1952
|
+
result = runner.invoke(cli, ["check", ".", *extra_args])
|
|
1953
|
+
|
|
1954
|
+
assert result.exit_code == 0
|
|
1955
|
+
output = result.stdout + result.stderr
|
|
1956
|
+
assert "nothing was checked" in output
|
|
1957
|
+
assert "No findings" not in output
|
|
1958
|
+
assert mock_run.call_count == expected_calls
|
|
1959
|
+
mock_run.reset_mock()
|
|
1960
|
+
|
|
1961
|
+
|
|
1962
|
+
@patch("shutil.which")
|
|
1963
|
+
@patch("subprocess.run")
|
|
1964
|
+
def test_biome_zero_file_path_is_not_hidden_by_a_checked_path(
|
|
1965
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1966
|
+
) -> None:
|
|
1967
|
+
"""A clean aggregate identifies requested paths Biome did not inspect."""
|
|
1968
|
+
mock_which.side_effect = lambda x: x if x == "biome" else None
|
|
1969
|
+
checked_files = MagicMock(returncode=0, stdout="", stderr="")
|
|
1970
|
+
checked_nothing = MagicMock(
|
|
1971
|
+
returncode=0,
|
|
1972
|
+
stdout="Checked 0 files in 1ms. No fixes applied.\n",
|
|
1973
|
+
stderr="",
|
|
1974
|
+
)
|
|
1975
|
+
|
|
1976
|
+
runner = CliRunner()
|
|
1977
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1978
|
+
Path(".gitignore").write_text("ignored/\n")
|
|
1979
|
+
Path("src").mkdir()
|
|
1980
|
+
Path("src/good.js").touch()
|
|
1981
|
+
Path("ignored").mkdir()
|
|
1982
|
+
Path("ignored/generated.js").touch()
|
|
1983
|
+
mock_run.side_effect = [
|
|
1984
|
+
checked_files,
|
|
1985
|
+
checked_files,
|
|
1986
|
+
checked_nothing,
|
|
1987
|
+
checked_nothing,
|
|
1988
|
+
]
|
|
1989
|
+
|
|
1990
|
+
result = runner.invoke(cli, ["check", "src", "ignored"])
|
|
1991
|
+
|
|
1992
|
+
assert result.exit_code == 0
|
|
1993
|
+
assert "nothing was checked for 1 path(s): ignored" in result.stderr
|
|
1994
|
+
assert "No findings in 1 path(s) (biome)." in result.stderr
|
|
1995
|
+
assert "No findings in 2 path(s)" not in result.stderr
|
|
1996
|
+
|
|
1997
|
+
|
|
1998
|
+
@patch("shutil.which")
|
|
1999
|
+
@patch("subprocess.run")
|
|
2000
|
+
def test_biome_zero_file_failure_still_exits_nonzero(
|
|
2001
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2002
|
+
) -> None:
|
|
2003
|
+
"""A configuration failure must not become a successful no-tool result."""
|
|
2004
|
+
mock_which.side_effect = lambda x: x if x == "biome" else None
|
|
2005
|
+
checked_nothing = MagicMock(
|
|
2006
|
+
returncode=1,
|
|
2007
|
+
stdout="Checked 0 files in 1ms.\nConfiguration error.\n",
|
|
2008
|
+
stderr="",
|
|
2009
|
+
)
|
|
2010
|
+
formatted_nothing = MagicMock(
|
|
2011
|
+
returncode=1,
|
|
2012
|
+
stdout="Formatted 0 files in 1ms.\nConfiguration error.\n",
|
|
2013
|
+
stderr="",
|
|
2014
|
+
)
|
|
2015
|
+
|
|
2016
|
+
runner = CliRunner()
|
|
2017
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2018
|
+
Path("script.ts").touch()
|
|
2019
|
+
|
|
2020
|
+
for extra_args in ([], ["--fix"]):
|
|
2021
|
+
mock_run.side_effect = (
|
|
2022
|
+
[checked_nothing, checked_nothing]
|
|
2023
|
+
if not extra_args
|
|
2024
|
+
else [formatted_nothing, checked_nothing, checked_nothing]
|
|
2025
|
+
)
|
|
2026
|
+
result = runner.invoke(cli, ["check", "script.ts", *extra_args])
|
|
2027
|
+
|
|
2028
|
+
assert result.exit_code == 1
|
|
2029
|
+
output = result.stdout + result.stderr
|
|
2030
|
+
assert "biome findings" in output
|
|
2031
|
+
assert "nothing was checked" not in output
|
|
2032
|
+
mock_run.reset_mock()
|
|
2033
|
+
|
|
2034
|
+
|
|
2035
|
+
@patch("shutil.which")
|
|
2036
|
+
@patch("subprocess.run")
|
|
2037
|
+
def test_bundled_biome_applies_to_a_nested_web_file(
|
|
2038
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2039
|
+
) -> None:
|
|
2040
|
+
"""Directory applicability still finds nested web files."""
|
|
2041
|
+
mock_which.side_effect = lambda x: x if x == "biome" else None
|
|
2042
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2043
|
+
|
|
2044
|
+
runner = CliRunner()
|
|
2045
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2046
|
+
Path("src").mkdir()
|
|
2047
|
+
Path("src/script.ts").touch()
|
|
2048
|
+
|
|
2049
|
+
result = runner.invoke(cli, ["check", "."])
|
|
2050
|
+
|
|
2051
|
+
assert result.exit_code == 0
|
|
2052
|
+
assert "No findings in 1 path(s) (biome)." in result.stderr
|
|
1806
2053
|
called = [call.args[0][0] for call in mock_run.call_args_list]
|
|
1807
|
-
assert "
|
|
2054
|
+
assert called == ["biome", "biome"]
|
|
1808
2055
|
|
|
1809
2056
|
|
|
1810
2057
|
@patch("shutil.which")
|
|
1811
2058
|
@patch("subprocess.run")
|
|
1812
|
-
def
|
|
2059
|
+
def test_check_with_project_config_runs_biome(
|
|
1813
2060
|
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1814
2061
|
) -> None:
|
|
1815
2062
|
"""With biome.json present, biome runs as it always did."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|