readability-cli 0.8.2__tar.gz → 0.8.4__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.4}/PKG-INFO +25 -17
- {readability_cli-0.8.2 → readability_cli-0.8.4}/README.md +24 -16
- readability_cli-0.8.4/configs/biome-default.json +14 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/guides/jsoncstyleguide.md +1 -1
- {readability_cli-0.8.2 → readability_cli-0.8.4}/pyproject.toml +1 -1
- {readability_cli-0.8.2 → readability_cli-0.8.4}/readability.py +117 -41
- {readability_cli-0.8.2 → readability_cli-0.8.4}/test_readability.py +336 -7
- {readability_cli-0.8.2 → readability_cli-0.8.4}/uv.lock +1 -1
- {readability_cli-0.8.2 → readability_cli-0.8.4}/.github/workflows/ci.yml +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/.github/workflows/publish.yml +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/.github/workflows/update-guides.yml +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/.gitignore +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/.python-version +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/LICENSE +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/configs/pyrefly.toml +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/configs/ruff.toml +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/guides/Rguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/guides/cppguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/guides/csharp-style.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/guides/docguide-style.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/guides/go-guide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/guides/htmlcssguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/guides/javaguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/guides/jsguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/guides/objcguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/guides/pyguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/guides/shellguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/guides/tsguide.md +0 -0
- {readability_cli-0.8.2 → readability_cli-0.8.4}/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.4
|
|
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
|
|
|
@@ -123,7 +123,7 @@ Property names must conform to the following guidelines:
|
|
|
123
123
|
|
|
124
124
|
* Property names should be meaningful names with defined semantics.
|
|
125
125
|
* Property names must be camel-cased, ascii strings.
|
|
126
|
-
* The first character must be a letter, an underscore (
|
|
126
|
+
* The first character must be a letter, an underscore (_) or a dollar sign ($).
|
|
127
127
|
* Subsequent characters can be a letter, a digit, an underscore, or a dollar sign.
|
|
128
128
|
* Reserved JavaScript keywords should be avoided (A list of reserved JavaScript keywords can be found below).
|
|
129
129
|
|
|
@@ -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,32 @@ def _get_tool_definitions(
|
|
|
1456
1486
|
pyrefly_config = _default_config_args(
|
|
1457
1487
|
project_root, ["pyrefly.toml"], "pyrefly"
|
|
1458
1488
|
)
|
|
1489
|
+
# An explicit path switches Pyrefly to single-file mode, where the
|
|
1490
|
+
# project's includes, excludes, and project-rooted import resolution are
|
|
1491
|
+
# deliberately ignored. A directory backed by project-owned settings is
|
|
1492
|
+
# a project check, so let Pyrefly discover its files from that config.
|
|
1493
|
+
pyrefly_project_mode = path.is_dir() and not pyrefly_config
|
|
1494
|
+
pyrefly_targets = [] if pyrefly_project_mode else [path_str]
|
|
1495
|
+
biome_config = []
|
|
1496
|
+
if not any(
|
|
1497
|
+
(project_root / filename).exists()
|
|
1498
|
+
for filename in ("biome.json", "biome.jsonc")
|
|
1499
|
+
):
|
|
1500
|
+
biome_config = ["--config-path", str(_bundled_config("biome"))]
|
|
1501
|
+
ignore_files = (
|
|
1502
|
+
project_root / ".gitignore",
|
|
1503
|
+
project_root / ".ignore",
|
|
1504
|
+
project_root / ".git" / "info" / "exclude",
|
|
1505
|
+
)
|
|
1506
|
+
if any(ignore_file.is_file() for ignore_file in ignore_files):
|
|
1507
|
+
biome_config.extend(
|
|
1508
|
+
[
|
|
1509
|
+
"--vcs-enabled=true",
|
|
1510
|
+
"--vcs-client-kind=git",
|
|
1511
|
+
"--vcs-use-ignore-file=true",
|
|
1512
|
+
f"--vcs-root={project_root}",
|
|
1513
|
+
]
|
|
1514
|
+
)
|
|
1459
1515
|
|
|
1460
1516
|
# Resolved once so every command for a tool reaches the same executable
|
|
1461
1517
|
ruff = _tool_command("ruff", project_root)
|
|
@@ -1502,7 +1558,13 @@ def _get_tool_definitions(
|
|
|
1502
1558
|
{
|
|
1503
1559
|
# Type checker only: it reports findings but cannot fix or format
|
|
1504
1560
|
"name": "pyrefly",
|
|
1505
|
-
"check": [
|
|
1561
|
+
"check": [
|
|
1562
|
+
*pyrefly,
|
|
1563
|
+
"check",
|
|
1564
|
+
*pyrefly_config,
|
|
1565
|
+
*pyrefly_targets,
|
|
1566
|
+
],
|
|
1567
|
+
**({"cwd": project_root} if pyrefly_project_mode else {}),
|
|
1506
1568
|
"trigger": ["pyproject.toml", "pyrefly.toml"],
|
|
1507
1569
|
"extensions": [".py"],
|
|
1508
1570
|
},
|
|
@@ -1511,12 +1573,14 @@ def _get_tool_definitions(
|
|
|
1511
1573
|
"check": [
|
|
1512
1574
|
*biome,
|
|
1513
1575
|
"lint",
|
|
1576
|
+
*biome_config,
|
|
1514
1577
|
"--no-errors-on-unmatched",
|
|
1515
1578
|
path_str,
|
|
1516
1579
|
],
|
|
1517
1580
|
"check_format": [
|
|
1518
1581
|
*biome,
|
|
1519
1582
|
"format",
|
|
1583
|
+
*biome_config,
|
|
1520
1584
|
"--no-errors-on-unmatched",
|
|
1521
1585
|
path_str,
|
|
1522
1586
|
],
|
|
@@ -1524,6 +1588,7 @@ def _get_tool_definitions(
|
|
|
1524
1588
|
*biome,
|
|
1525
1589
|
"lint",
|
|
1526
1590
|
"--write",
|
|
1591
|
+
*biome_config,
|
|
1527
1592
|
"--no-errors-on-unmatched",
|
|
1528
1593
|
path_str,
|
|
1529
1594
|
],
|
|
@@ -1531,6 +1596,7 @@ def _get_tool_definitions(
|
|
|
1531
1596
|
*biome,
|
|
1532
1597
|
"format",
|
|
1533
1598
|
"--write",
|
|
1599
|
+
*biome_config,
|
|
1534
1600
|
"--no-errors-on-unmatched",
|
|
1535
1601
|
path_str,
|
|
1536
1602
|
],
|
|
@@ -1614,6 +1680,7 @@ def _run_tool(
|
|
|
1614
1680
|
logger.info("Running %s...", tool_name)
|
|
1615
1681
|
|
|
1616
1682
|
report = CheckReport()
|
|
1683
|
+
cwd = tool_config.get("cwd")
|
|
1617
1684
|
try:
|
|
1618
1685
|
if fix:
|
|
1619
1686
|
# Formatters rewrite files, fixers apply what they can. Both
|
|
@@ -1622,11 +1689,13 @@ def _run_tool(
|
|
|
1622
1689
|
# and report what they could not deal with.
|
|
1623
1690
|
for phase in ("format", "fix"):
|
|
1624
1691
|
if phase in tool_config:
|
|
1625
|
-
|
|
1626
|
-
|
|
1692
|
+
result = _capture_tool_command(tool_config[phase], cwd=cwd)
|
|
1693
|
+
if _tool_checked_files(tool_name, result):
|
|
1694
|
+
report.ran.add(tool_name)
|
|
1627
1695
|
elif "check_format" in tool_config:
|
|
1628
|
-
result = _capture_tool_command(tool_config["check_format"])
|
|
1629
|
-
|
|
1696
|
+
result = _capture_tool_command(tool_config["check_format"], cwd=cwd)
|
|
1697
|
+
if _tool_checked_files(tool_name, result):
|
|
1698
|
+
report.ran.add(tool_name)
|
|
1630
1699
|
# gofmt reports by naming files rather than by exit code
|
|
1631
1700
|
if result.returncode != 0 or (
|
|
1632
1701
|
tool_name == "go fmt" and result.stdout.strip()
|
|
@@ -1638,8 +1707,9 @@ def _run_tool(
|
|
|
1638
1707
|
)
|
|
1639
1708
|
|
|
1640
1709
|
if "check" in tool_config:
|
|
1641
|
-
result = _capture_tool_command(tool_config["check"])
|
|
1642
|
-
|
|
1710
|
+
result = _capture_tool_command(tool_config["check"], cwd=cwd)
|
|
1711
|
+
if _tool_checked_files(tool_name, result):
|
|
1712
|
+
report.ran.add(tool_name)
|
|
1643
1713
|
if result.returncode != 0:
|
|
1644
1714
|
report.findings = True
|
|
1645
1715
|
click.echo(
|
|
@@ -1656,14 +1726,38 @@ def _run_tool(
|
|
|
1656
1726
|
return report
|
|
1657
1727
|
|
|
1658
1728
|
|
|
1659
|
-
def
|
|
1729
|
+
def _tool_checked_files(
|
|
1730
|
+
tool_name: str, result: subprocess.CompletedProcess
|
|
1731
|
+
) -> bool:
|
|
1732
|
+
"""Report whether a completed command actually processed any files.
|
|
1733
|
+
|
|
1734
|
+
Args:
|
|
1735
|
+
tool_name: The tool whose command completed.
|
|
1736
|
+
result: The completed subprocess.
|
|
1737
|
+
|
|
1738
|
+
Returns:
|
|
1739
|
+
False when Biome explicitly reports that it checked zero files.
|
|
1740
|
+
"""
|
|
1741
|
+
if tool_name != "biome":
|
|
1742
|
+
return True
|
|
1743
|
+
output = f"{result.stdout or ''}\n{result.stderr or ''}"
|
|
1744
|
+
# Biome 1.x and 2.x use these summaries for an unmatched target.
|
|
1745
|
+
zero_file_summaries = ("Checked 0 files", "Formatted 0 files")
|
|
1746
|
+
return not any(summary in output for summary in zero_file_summaries)
|
|
1747
|
+
|
|
1748
|
+
|
|
1749
|
+
def _capture_tool_command(
|
|
1750
|
+
cmd: list[str], cwd: Path | None = None
|
|
1751
|
+
) -> subprocess.CompletedProcess:
|
|
1660
1752
|
"""Run a tool and capture what it said, whatever its exit code.
|
|
1661
1753
|
|
|
1662
1754
|
Args:
|
|
1663
1755
|
cmd: The command list to execute.
|
|
1756
|
+
cwd: Directory in which the tool should run. Defaults to the caller's
|
|
1757
|
+
current working directory.
|
|
1664
1758
|
|
|
1665
1759
|
Returns:
|
|
1666
|
-
The completed process,
|
|
1760
|
+
The completed process, including captured output and its exit code.
|
|
1667
1761
|
|
|
1668
1762
|
Raises:
|
|
1669
1763
|
subprocess.SubprocessError: If the command outlives the timeout.
|
|
@@ -1676,25 +1770,7 @@ def _capture_tool_command(cmd: list[str]) -> subprocess.CompletedProcess:
|
|
|
1676
1770
|
text=True,
|
|
1677
1771
|
check=False,
|
|
1678
1772
|
timeout=DEFAULT_TIMEOUT,
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
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
|
|
1773
|
+
cwd=cwd,
|
|
1698
1774
|
)
|
|
1699
1775
|
|
|
1700
1776
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import logging
|
|
2
3
|
import os
|
|
3
4
|
import subprocess
|
|
@@ -458,6 +459,36 @@ def test_check_command_pyrefly(
|
|
|
458
459
|
assert ["pyrefly", "check", "--config", cfg, "script.py"] in called_cmds
|
|
459
460
|
|
|
460
461
|
|
|
462
|
+
@patch("shutil.which")
|
|
463
|
+
@patch("subprocess.run")
|
|
464
|
+
def test_check_command_runs_configured_directory_as_pyrefly_project(
|
|
465
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
466
|
+
) -> None:
|
|
467
|
+
"""The CLI lets project-owned includes and excludes select Python files."""
|
|
468
|
+
mock_which.side_effect = lambda name: name if name == "pyrefly" else None
|
|
469
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
470
|
+
|
|
471
|
+
runner = CliRunner()
|
|
472
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
473
|
+
Path("pyrefly.toml").write_text(
|
|
474
|
+
'project-excludes = ["**/excluded.py"]\n'
|
|
475
|
+
)
|
|
476
|
+
Path("main.py").touch()
|
|
477
|
+
project_root = Path.cwd()
|
|
478
|
+
|
|
479
|
+
result = runner.invoke(cli, ["check", "."])
|
|
480
|
+
|
|
481
|
+
assert result.exit_code == 0
|
|
482
|
+
pyrefly_calls = [
|
|
483
|
+
invocation
|
|
484
|
+
for invocation in mock_run.call_args_list
|
|
485
|
+
if invocation.args[0][0] == "pyrefly"
|
|
486
|
+
]
|
|
487
|
+
assert len(pyrefly_calls) == 1
|
|
488
|
+
assert pyrefly_calls[0].args[0] == ["pyrefly", "check"]
|
|
489
|
+
assert pyrefly_calls[0].kwargs["cwd"] == project_root
|
|
490
|
+
|
|
491
|
+
|
|
461
492
|
def test_has_project_config(tmp_path: Path) -> None:
|
|
462
493
|
"""Tests project config detection via dedicated files and pyproject.
|
|
463
494
|
|
|
@@ -505,15 +536,124 @@ def test_default_configs_omitted_when_project_configured(
|
|
|
505
536
|
assert "--config" not in tools["ruff"]["format"]
|
|
506
537
|
assert "--config" not in tools["pyrefly"]["check"]
|
|
507
538
|
|
|
539
|
+
ts_file = tmp_path / "script.ts"
|
|
540
|
+
ts_file.touch()
|
|
541
|
+
(tmp_path / "biome.json").write_text("{}")
|
|
542
|
+
tools = {t["name"]: t for t in _get_tool_definitions(ts_file, tmp_path)}
|
|
543
|
+
for command in ("check", "check_format", "fix", "format"):
|
|
544
|
+
assert "--config-path" not in tools["biome"][command]
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def test_pyrefly_uses_project_mode_only_for_configured_directories(
|
|
548
|
+
tmp_path: Path,
|
|
549
|
+
) -> None:
|
|
550
|
+
"""Directories use config discovery while explicit files stay explicit."""
|
|
551
|
+
(tmp_path / "pyrefly.toml").write_text(
|
|
552
|
+
'project-excludes = ["**/excluded.py"]\n'
|
|
553
|
+
)
|
|
554
|
+
py_file = tmp_path / "main.py"
|
|
555
|
+
py_file.touch()
|
|
556
|
+
|
|
557
|
+
with patch("shutil.which", side_effect=lambda name: name):
|
|
558
|
+
directory_tools = {
|
|
559
|
+
tool["name"]: tool
|
|
560
|
+
for tool in _get_tool_definitions(tmp_path, tmp_path)
|
|
561
|
+
}
|
|
562
|
+
file_tools = {
|
|
563
|
+
tool["name"]: tool
|
|
564
|
+
for tool in _get_tool_definitions(py_file, tmp_path)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
assert directory_tools["pyrefly"]["check"] == ["pyrefly", "check"]
|
|
568
|
+
assert directory_tools["pyrefly"]["cwd"] == tmp_path
|
|
569
|
+
assert file_tools["pyrefly"]["check"] == [
|
|
570
|
+
"pyrefly",
|
|
571
|
+
"check",
|
|
572
|
+
str(py_file),
|
|
573
|
+
]
|
|
574
|
+
assert "cwd" not in file_tools["pyrefly"]
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def test_pyrefly_bundled_config_keeps_an_explicit_directory(
|
|
578
|
+
tmp_path: Path,
|
|
579
|
+
) -> None:
|
|
580
|
+
"""Project mode cannot use a config rooted in the installed package."""
|
|
581
|
+
(tmp_path / "main.py").touch()
|
|
582
|
+
|
|
583
|
+
with patch("shutil.which", side_effect=lambda name: name):
|
|
584
|
+
tools = {
|
|
585
|
+
tool["name"]: tool
|
|
586
|
+
for tool in _get_tool_definitions(tmp_path, tmp_path)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
assert tools["pyrefly"]["check"] == [
|
|
590
|
+
"pyrefly",
|
|
591
|
+
"check",
|
|
592
|
+
"--config",
|
|
593
|
+
str(_bundled_config("pyrefly")),
|
|
594
|
+
str(tmp_path),
|
|
595
|
+
]
|
|
596
|
+
assert "cwd" not in tools["pyrefly"]
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def test_biome_bundled_config_is_injected_into_every_command(
|
|
600
|
+
tmp_path: Path,
|
|
601
|
+
) -> None:
|
|
602
|
+
"""Linting, formatting, and their fix forms share the safe default."""
|
|
603
|
+
ts_file = tmp_path / "script.ts"
|
|
604
|
+
ts_file.touch()
|
|
508
605
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
606
|
+
tools = {t["name"]: t for t in _get_tool_definitions(ts_file, tmp_path)}
|
|
607
|
+
config_path = str(_bundled_config("biome"))
|
|
608
|
+
|
|
609
|
+
for command_name in ("check", "check_format", "fix", "format"):
|
|
610
|
+
command = tools["biome"][command_name]
|
|
611
|
+
assert command[command.index("--config-path") + 1] == config_path
|
|
612
|
+
assert "--vcs-enabled=true" not in command
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
@pytest.mark.parametrize(
|
|
616
|
+
"ignore_file", (".gitignore", ".ignore", ".git/info/exclude")
|
|
617
|
+
)
|
|
618
|
+
def test_biome_bundled_config_respects_project_ignore_files(
|
|
619
|
+
tmp_path: Path, ignore_file: str
|
|
620
|
+
) -> None:
|
|
621
|
+
"""An external default keeps ignore discovery rooted in the project."""
|
|
622
|
+
ts_file = tmp_path / "script.ts"
|
|
623
|
+
ts_file.touch()
|
|
624
|
+
ignore_path = tmp_path / ignore_file
|
|
625
|
+
ignore_path.parent.mkdir(parents=True, exist_ok=True)
|
|
626
|
+
ignore_path.write_text("node_modules/\n")
|
|
627
|
+
|
|
628
|
+
tools = {t["name"]: t for t in _get_tool_definitions(ts_file, tmp_path)}
|
|
629
|
+
|
|
630
|
+
for command_name in ("check", "check_format", "fix", "format"):
|
|
631
|
+
command = tools["biome"][command_name]
|
|
632
|
+
assert "--vcs-enabled=true" in command
|
|
633
|
+
assert "--vcs-client-kind=git" in command
|
|
634
|
+
assert "--vcs-use-ignore-file=true" in command
|
|
635
|
+
assert f"--vcs-root={tmp_path}" in command
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def test_bundled_default_configs_are_valid() -> None:
|
|
639
|
+
"""Tests that the bundled default configs exist and parse."""
|
|
640
|
+
# Both Python configs must exist and be valid TOML
|
|
512
641
|
for tool in ("ruff", "pyrefly"):
|
|
513
642
|
config_path = _bundled_config(tool)
|
|
514
643
|
assert config_path.exists()
|
|
515
644
|
tomllib.loads(config_path.read_text())
|
|
516
645
|
|
|
646
|
+
biome_config_path = _bundled_config("biome")
|
|
647
|
+
assert biome_config_path.exists()
|
|
648
|
+
biome_config = json.loads(biome_config_path.read_text())
|
|
649
|
+
assert biome_config["formatter"] == {
|
|
650
|
+
"enabled": True,
|
|
651
|
+
"indentStyle": "space",
|
|
652
|
+
"indentWidth": 2,
|
|
653
|
+
"lineWidth": 80,
|
|
654
|
+
}
|
|
655
|
+
assert biome_config["linter"]["rules"]["recommended"] is True
|
|
656
|
+
|
|
517
657
|
# The ruff defaults follow the Google Python style guide
|
|
518
658
|
ruff_config = tomllib.loads(_bundled_config("ruff").read_text())
|
|
519
659
|
assert ruff_config["line-length"] == 80
|
|
@@ -560,6 +700,29 @@ def test_check_paths_aggregates_str_and_path_inputs(
|
|
|
560
700
|
]
|
|
561
701
|
|
|
562
702
|
|
|
703
|
+
@patch("readability._check_path")
|
|
704
|
+
def test_check_paths_preserves_unverified_paths_when_another_path_ran(
|
|
705
|
+
mock_check_path: MagicMock,
|
|
706
|
+
tmp_path: Path,
|
|
707
|
+
) -> None:
|
|
708
|
+
"""One checked path must not hide another path that processed no files."""
|
|
709
|
+
checked = tmp_path / "src"
|
|
710
|
+
unchecked = tmp_path / "ignored"
|
|
711
|
+
checked.mkdir()
|
|
712
|
+
unchecked.mkdir()
|
|
713
|
+
mock_check_path.side_effect = [
|
|
714
|
+
CheckReport(ran={"biome"}),
|
|
715
|
+
CheckReport(),
|
|
716
|
+
]
|
|
717
|
+
|
|
718
|
+
report = check_paths([checked, unchecked], project_root=tmp_path)
|
|
719
|
+
|
|
720
|
+
assert report == CheckReport(
|
|
721
|
+
ran={"biome"},
|
|
722
|
+
unverified_paths=[unchecked],
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
|
|
563
726
|
@patch("readability._check_path", return_value=CheckReport(ran={"ruff"}))
|
|
564
727
|
def test_check_paths_defaults_project_root_to_cwd(
|
|
565
728
|
mock_check_path: MagicMock,
|
|
@@ -1789,10 +1952,10 @@ def test_check_runs_bundled_default_tools_without_a_trigger(
|
|
|
1789
1952
|
|
|
1790
1953
|
@patch("shutil.which")
|
|
1791
1954
|
@patch("subprocess.run")
|
|
1792
|
-
def
|
|
1955
|
+
def test_check_runs_biome_without_a_project_config(
|
|
1793
1956
|
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1794
1957
|
) -> None:
|
|
1795
|
-
"""
|
|
1958
|
+
"""A bundled Biome config covers web files in unconfigured projects."""
|
|
1796
1959
|
mock_which.side_effect = lambda x: x
|
|
1797
1960
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
1798
1961
|
|
|
@@ -1803,13 +1966,179 @@ def test_check_still_gates_tools_without_bundled_defaults(
|
|
|
1803
1966
|
result = runner.invoke(cli, ["check", "script.ts"])
|
|
1804
1967
|
|
|
1805
1968
|
assert result.exit_code == 0
|
|
1969
|
+
called = [call.args[0] for call in mock_run.call_args_list]
|
|
1970
|
+
assert any(command[0] == "biome" for command in called)
|
|
1971
|
+
assert all("prettier" not in command for command in called)
|
|
1972
|
+
config_path = str(_bundled_config("biome"))
|
|
1973
|
+
biome_commands = [command for command in called if command[0] == "biome"]
|
|
1974
|
+
assert biome_commands
|
|
1975
|
+
assert all(
|
|
1976
|
+
command[command.index("--config-path") + 1] == config_path
|
|
1977
|
+
for command in biome_commands
|
|
1978
|
+
)
|
|
1979
|
+
|
|
1980
|
+
|
|
1981
|
+
@patch("shutil.which")
|
|
1982
|
+
@patch("subprocess.run")
|
|
1983
|
+
def test_bundled_tools_do_not_claim_an_unsupported_directory(
|
|
1984
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1985
|
+
) -> None:
|
|
1986
|
+
"""A zero-file tool run must not turn nothing checked into a clean pass."""
|
|
1987
|
+
mock_which.side_effect = lambda x: x
|
|
1988
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
1989
|
+
|
|
1990
|
+
runner = CliRunner()
|
|
1991
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1992
|
+
Path("notes.md").touch()
|
|
1993
|
+
|
|
1994
|
+
result = runner.invoke(cli, ["check", "."])
|
|
1995
|
+
|
|
1996
|
+
assert result.exit_code == 0
|
|
1997
|
+
output = result.stdout + result.stderr
|
|
1998
|
+
assert "nothing was checked" in output
|
|
1999
|
+
assert "No findings" not in output
|
|
2000
|
+
mock_run.assert_not_called()
|
|
2001
|
+
|
|
2002
|
+
|
|
2003
|
+
@patch("shutil.which")
|
|
2004
|
+
@patch("subprocess.run")
|
|
2005
|
+
def test_biome_zero_file_result_is_not_reported_as_clean(
|
|
2006
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2007
|
+
) -> None:
|
|
2008
|
+
"""Ignored matches must not make an unverified directory look clean."""
|
|
2009
|
+
mock_which.side_effect = lambda x: x if x == "biome" else None
|
|
2010
|
+
checked_nothing = MagicMock(
|
|
2011
|
+
returncode=0,
|
|
2012
|
+
stdout="",
|
|
2013
|
+
stderr="Checked 0 files in 1ms. No fixes applied.\n",
|
|
2014
|
+
)
|
|
2015
|
+
formatted_nothing = MagicMock(
|
|
2016
|
+
returncode=0,
|
|
2017
|
+
stdout="Formatted 0 files in 1ms. No fixes applied.\n",
|
|
2018
|
+
stderr="",
|
|
2019
|
+
)
|
|
2020
|
+
|
|
2021
|
+
runner = CliRunner()
|
|
2022
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2023
|
+
Path(".gitignore").write_text("node_modules/\n")
|
|
2024
|
+
Path("node_modules/example").mkdir(parents=True)
|
|
2025
|
+
Path("node_modules/example/index.js").touch()
|
|
2026
|
+
Path("README.md").touch()
|
|
2027
|
+
|
|
2028
|
+
for extra_args, expected_calls in (([], 2), (["--fix"], 3)):
|
|
2029
|
+
mock_run.side_effect = (
|
|
2030
|
+
[checked_nothing, checked_nothing]
|
|
2031
|
+
if not extra_args
|
|
2032
|
+
else [formatted_nothing, checked_nothing, checked_nothing]
|
|
2033
|
+
)
|
|
2034
|
+
result = runner.invoke(cli, ["check", ".", *extra_args])
|
|
2035
|
+
|
|
2036
|
+
assert result.exit_code == 0
|
|
2037
|
+
output = result.stdout + result.stderr
|
|
2038
|
+
assert "nothing was checked" in output
|
|
2039
|
+
assert "No findings" not in output
|
|
2040
|
+
assert mock_run.call_count == expected_calls
|
|
2041
|
+
mock_run.reset_mock()
|
|
2042
|
+
|
|
2043
|
+
|
|
2044
|
+
@patch("shutil.which")
|
|
2045
|
+
@patch("subprocess.run")
|
|
2046
|
+
def test_biome_zero_file_path_is_not_hidden_by_a_checked_path(
|
|
2047
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2048
|
+
) -> None:
|
|
2049
|
+
"""A clean aggregate identifies requested paths Biome did not inspect."""
|
|
2050
|
+
mock_which.side_effect = lambda x: x if x == "biome" else None
|
|
2051
|
+
checked_files = MagicMock(returncode=0, stdout="", stderr="")
|
|
2052
|
+
checked_nothing = MagicMock(
|
|
2053
|
+
returncode=0,
|
|
2054
|
+
stdout="Checked 0 files in 1ms. No fixes applied.\n",
|
|
2055
|
+
stderr="",
|
|
2056
|
+
)
|
|
2057
|
+
|
|
2058
|
+
runner = CliRunner()
|
|
2059
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2060
|
+
Path(".gitignore").write_text("ignored/\n")
|
|
2061
|
+
Path("src").mkdir()
|
|
2062
|
+
Path("src/good.js").touch()
|
|
2063
|
+
Path("ignored").mkdir()
|
|
2064
|
+
Path("ignored/generated.js").touch()
|
|
2065
|
+
mock_run.side_effect = [
|
|
2066
|
+
checked_files,
|
|
2067
|
+
checked_files,
|
|
2068
|
+
checked_nothing,
|
|
2069
|
+
checked_nothing,
|
|
2070
|
+
]
|
|
2071
|
+
|
|
2072
|
+
result = runner.invoke(cli, ["check", "src", "ignored"])
|
|
2073
|
+
|
|
2074
|
+
assert result.exit_code == 0
|
|
2075
|
+
assert "nothing was checked for 1 path(s): ignored" in result.stderr
|
|
2076
|
+
assert "No findings in 1 path(s) (biome)." in result.stderr
|
|
2077
|
+
assert "No findings in 2 path(s)" not in result.stderr
|
|
2078
|
+
|
|
2079
|
+
|
|
2080
|
+
@patch("shutil.which")
|
|
2081
|
+
@patch("subprocess.run")
|
|
2082
|
+
def test_biome_zero_file_failure_still_exits_nonzero(
|
|
2083
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2084
|
+
) -> None:
|
|
2085
|
+
"""A configuration failure must not become a successful no-tool result."""
|
|
2086
|
+
mock_which.side_effect = lambda x: x if x == "biome" else None
|
|
2087
|
+
checked_nothing = MagicMock(
|
|
2088
|
+
returncode=1,
|
|
2089
|
+
stdout="Checked 0 files in 1ms.\nConfiguration error.\n",
|
|
2090
|
+
stderr="",
|
|
2091
|
+
)
|
|
2092
|
+
formatted_nothing = MagicMock(
|
|
2093
|
+
returncode=1,
|
|
2094
|
+
stdout="Formatted 0 files in 1ms.\nConfiguration error.\n",
|
|
2095
|
+
stderr="",
|
|
2096
|
+
)
|
|
2097
|
+
|
|
2098
|
+
runner = CliRunner()
|
|
2099
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2100
|
+
Path("script.ts").touch()
|
|
2101
|
+
|
|
2102
|
+
for extra_args in ([], ["--fix"]):
|
|
2103
|
+
mock_run.side_effect = (
|
|
2104
|
+
[checked_nothing, checked_nothing]
|
|
2105
|
+
if not extra_args
|
|
2106
|
+
else [formatted_nothing, checked_nothing, checked_nothing]
|
|
2107
|
+
)
|
|
2108
|
+
result = runner.invoke(cli, ["check", "script.ts", *extra_args])
|
|
2109
|
+
|
|
2110
|
+
assert result.exit_code == 1
|
|
2111
|
+
output = result.stdout + result.stderr
|
|
2112
|
+
assert "biome findings" in output
|
|
2113
|
+
assert "nothing was checked" not in output
|
|
2114
|
+
mock_run.reset_mock()
|
|
2115
|
+
|
|
2116
|
+
|
|
2117
|
+
@patch("shutil.which")
|
|
2118
|
+
@patch("subprocess.run")
|
|
2119
|
+
def test_bundled_biome_applies_to_a_nested_web_file(
|
|
2120
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2121
|
+
) -> None:
|
|
2122
|
+
"""Directory applicability still finds nested web files."""
|
|
2123
|
+
mock_which.side_effect = lambda x: x if x == "biome" else None
|
|
2124
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2125
|
+
|
|
2126
|
+
runner = CliRunner()
|
|
2127
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2128
|
+
Path("src").mkdir()
|
|
2129
|
+
Path("src/script.ts").touch()
|
|
2130
|
+
|
|
2131
|
+
result = runner.invoke(cli, ["check", "."])
|
|
2132
|
+
|
|
2133
|
+
assert result.exit_code == 0
|
|
2134
|
+
assert "No findings in 1 path(s) (biome)." in result.stderr
|
|
1806
2135
|
called = [call.args[0][0] for call in mock_run.call_args_list]
|
|
1807
|
-
assert "
|
|
2136
|
+
assert called == ["biome", "biome"]
|
|
1808
2137
|
|
|
1809
2138
|
|
|
1810
2139
|
@patch("shutil.which")
|
|
1811
2140
|
@patch("subprocess.run")
|
|
1812
|
-
def
|
|
2141
|
+
def test_check_with_project_config_runs_biome(
|
|
1813
2142
|
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1814
2143
|
) -> None:
|
|
1815
2144
|
"""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
|