readability-cli 0.10.2__tar.gz → 0.10.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.10.2/README.md → readability_cli-0.10.4/PKG-INFO +38 -15
- readability_cli-0.10.2/PKG-INFO → readability_cli-0.10.4/README.md +23 -29
- {readability_cli-0.10.2 → readability_cli-0.10.4}/pyproject.toml +10 -3
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/checking.py +3 -2
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/cli.py +8 -1
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/configs/ruff.toml +3 -2
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/guide.py +40 -25
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/tools.py +61 -2
- {readability_cli-0.10.2 → readability_cli-0.10.4}/test_checking.py +496 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/test_outline.py +17 -1
- {readability_cli-0.10.2 → readability_cli-0.10.4}/uv.lock +12 -5
- {readability_cli-0.10.2 → readability_cli-0.10.4}/.github/workflows/ci.yml +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/.github/workflows/publish.yml +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/.github/workflows/update-guides.yml +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/.gitignore +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/.python-version +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/LICENSE +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/__init__.py +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/configs/biome-default.json +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/configs/pyrefly.toml +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/guides/Rguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/guides/cppguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/guides/csharp-style.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/guides/docguide-style.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/guides/go-guide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/guides/htmlcssguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/guides/javaguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/guides/jsguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/guides/jsoncstyleguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/guides/objcguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/guides/pyguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/guides/shellguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/guides/tsguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/guides/vimscriptguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/readability/outline.py +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/test_guide.py +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.4}/test_package.py +0 -0
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: readability-cli
|
|
3
|
+
Version: 0.10.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
|
+
Project-URL: Homepage, https://github.com/owahltinez/readability
|
|
6
|
+
Project-URL: Repository, https://github.com/owahltinez/readability
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: click>=8.3.1
|
|
10
|
+
Provides-Extra: sync
|
|
11
|
+
Requires-Dist: beautifulsoup4>=4.14.3; extra == 'sync'
|
|
12
|
+
Requires-Dist: markdownify>=1.2.2; extra == 'sync'
|
|
13
|
+
Requires-Dist: requests>=2.33.0; extra == 'sync'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
1
16
|
# Readability
|
|
2
17
|
|
|
3
18
|
A CLI tool that keeps code aligned with Google style conventions. It runs the
|
|
@@ -9,7 +24,8 @@ quick access to style conventions without browsing HTML pages.
|
|
|
9
24
|
## Features
|
|
10
25
|
|
|
11
26
|
- **Linting & Formatting**: A `check` command that automatically detects and
|
|
12
|
-
runs relevant tools (Ruff, Pyrefly, Biome,
|
|
27
|
+
runs relevant tools (Ruff, Pyrefly, Biome, gofmt, goimports, shfmt,
|
|
28
|
+
shellcheck, and rustfmt) for your project.
|
|
13
29
|
- **Predictable Ownership**: Every supported format has fixed tools, so
|
|
14
30
|
configuration customizes checks without changing which formatter runs.
|
|
15
31
|
- **Style Guides**: A `guide` command that fetches the latest Google style
|
|
@@ -84,7 +100,7 @@ readability check . --unsafe
|
|
|
84
100
|
Fixes that may change a program's behavior or drop its comments are opt-in
|
|
85
101
|
through `--unsafe`, which every tool applies under its own name: Ruff's
|
|
86
102
|
`--unsafe-fixes` and Biome's `--unsafe`. Tools drawing no such distinction,
|
|
87
|
-
such as gofmt, are unaffected by it.
|
|
103
|
+
such as gofmt and goimports, are unaffected by it.
|
|
88
104
|
|
|
89
105
|
### Supported Formats
|
|
90
106
|
|
|
@@ -92,7 +108,9 @@ such as gofmt, are unaffected by it.
|
|
|
92
108
|
| ---------------------------------------------------------------- | --------------------------------------- | ------------------------------------- |
|
|
93
109
|
| `.py` | Ruff lint/format; Pyrefly type checking | Ruff and Pyrefly native configuration |
|
|
94
110
|
| `.js`, `.jsx`, `.ts`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` | Biome | `biome.json` or `biome.jsonc` |
|
|
95
|
-
| `.go` | gofmt
|
|
111
|
+
| `.go` | gofmt, goimports | None |
|
|
112
|
+
| `.sh`, `.bash` | shfmt format; shellcheck lint | None |
|
|
113
|
+
| `.rs` | rustfmt | None |
|
|
96
114
|
|
|
97
115
|
Markdown, YAML, SCSS, JSONL, and extensions not listed above are unsupported.
|
|
98
116
|
An unsupported-only path reports that nothing was checked and exits
|
|
@@ -100,8 +118,8 @@ successfully.
|
|
|
100
118
|
|
|
101
119
|
Biome first checks a project's `node_modules/.bin`. Ruff, Pyrefly, and Biome
|
|
102
120
|
then use `PATH`, followed by `uvx` for the Python tools or `npx` for Biome. The
|
|
103
|
-
runners cache downloads, so subsequent runs work offline. Gofmt
|
|
104
|
-
|
|
121
|
+
runners cache downloads, so subsequent runs work offline. Gofmt and goimports
|
|
122
|
+
must be available on `PATH`.
|
|
105
123
|
|
|
106
124
|
That keeps this package at ~4 MB rather than the ~54 MB it would take to carry
|
|
107
125
|
Ruff and Pyrefly itself — a cost that would fall on everyone using only `guide`.
|
|
@@ -119,8 +137,8 @@ Set `UV_OFFLINE=1` to forbid fetching. A tool that then cannot be reached fails
|
|
|
119
137
|
the run rather than passing it.
|
|
120
138
|
|
|
121
139
|
Ruff, Pyrefly, and Biome ship with bundled configurations, so they run on every
|
|
122
|
-
file they own without project setup. Gofmt
|
|
123
|
-
is no `go.mod`.
|
|
140
|
+
file they own without project setup. Gofmt and goimports run on `.go` files even
|
|
141
|
+
when there is no `go.mod`.
|
|
124
142
|
|
|
125
143
|
A tool that could not be reached at all is never skipped quietly:
|
|
126
144
|
|
|
@@ -175,8 +193,9 @@ caller happened to be standing in.
|
|
|
175
193
|
Ruff and Pyrefly defaults follow the
|
|
176
194
|
[Google Python style guide](https://google.github.io/styleguide/pyguide.html):
|
|
177
195
|
80-column lines, Google docstrings, one import per line sorted as isort's
|
|
178
|
-
`google` profile does,
|
|
179
|
-
|
|
196
|
+
`google` profile does, best-practice lint rules (bugbear, pyupgrade, simplify,
|
|
197
|
+
comprehensions, and Ruff-specific rules), and full type checking. The Biome
|
|
198
|
+
default applies the 80-column lines and two-space indentation of the
|
|
180
199
|
[Google JavaScript style guide](https://google.github.io/styleguide/jsguide.html)
|
|
181
200
|
and enables Biome's recommended lint rules. Project `biome.json` and
|
|
182
201
|
`biome.jsonc` files replace those bundled defaults for Biome-owned formats.
|
|
@@ -185,9 +204,9 @@ For Python, add `[tool.ruff]` or `[tool.pyrefly]` to `pyproject.toml`, or use
|
|
|
185
204
|
`ruff.toml`, `.ruff.toml`, or `pyrefly.toml`. Ruff, Pyrefly, and Biome load
|
|
186
205
|
their native project configurations in place of bundled defaults. Readability
|
|
187
206
|
does not interpret EditorConfig itself; a canonical tool such as Biome may opt
|
|
188
|
-
into it through that tool's native configuration.
|
|
189
|
-
The bundled Biome file requires Biome 2.5 or later,
|
|
190
|
-
runner's version floor.
|
|
207
|
+
into it through that tool's native configuration. Neither gofmt nor goimports
|
|
208
|
+
has project settings. The bundled Biome file requires Biome 2.5 or later,
|
|
209
|
+
matching the fallback runner's version floor.
|
|
191
210
|
|
|
192
211
|
Configuration is found per file, in the file's own directory and then its
|
|
193
212
|
ancestors, which is where each canonical tool looks. A package keeping its
|
|
@@ -354,9 +373,13 @@ Supported languages and their aliases:
|
|
|
354
373
|
- vim [cached]
|
|
355
374
|
```
|
|
356
375
|
|
|
357
|
-
Refresh the cache with `sync`, which takes languages or refetches everything
|
|
376
|
+
Refresh the cache with `sync`, which takes languages or refetches everything.
|
|
377
|
+
It fetches and converts HTML, so it needs the `sync` extra; the bundled guides
|
|
378
|
+
are already converted, and every other command reads those:
|
|
358
379
|
|
|
359
380
|
```bash
|
|
381
|
+
pip install 'readability-cli[sync]'
|
|
382
|
+
|
|
360
383
|
# Refetch one guide, or a few
|
|
361
384
|
readability sync python shell
|
|
362
385
|
|
|
@@ -371,8 +394,8 @@ The tool stores local copies of the style guides in the
|
|
|
371
394
|
the `guide` command uses these local files when they exist. The bundled copies
|
|
372
395
|
are automatically synchronized weekly from the official
|
|
373
396
|
[Google Style Guides](https://google.github.io/styleguide/) repository via
|
|
374
|
-
GitHub Actions,
|
|
375
|
-
command.
|
|
397
|
+
GitHub Actions, so upgrading the package is usually enough. To refresh a local
|
|
398
|
+
cache yourself, install the `sync` extra and run the `sync` command.
|
|
376
399
|
|
|
377
400
|
You can override the default guide directory by setting the
|
|
378
401
|
`READABILITY_CACHE` environment variable. This is useful if you want to store
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: readability-cli
|
|
3
|
-
Version: 0.10.2
|
|
4
|
-
Summary: A CLI to lint, format, and type-check code with Google-style defaults, and pull Google style guides in markdown format.
|
|
5
|
-
Project-URL: Homepage, https://github.com/owahltinez/readability
|
|
6
|
-
Project-URL: Repository, https://github.com/owahltinez/readability
|
|
7
|
-
License-File: LICENSE
|
|
8
|
-
Requires-Python: >=3.12
|
|
9
|
-
Requires-Dist: beautifulsoup4>=4.14.3
|
|
10
|
-
Requires-Dist: click>=8.3.1
|
|
11
|
-
Requires-Dist: markdownify>=1.2.2
|
|
12
|
-
Requires-Dist: requests>=2.33.0
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
|
|
15
1
|
# Readability
|
|
16
2
|
|
|
17
3
|
A CLI tool that keeps code aligned with Google style conventions. It runs the
|
|
@@ -23,7 +9,8 @@ quick access to style conventions without browsing HTML pages.
|
|
|
23
9
|
## Features
|
|
24
10
|
|
|
25
11
|
- **Linting & Formatting**: A `check` command that automatically detects and
|
|
26
|
-
runs relevant tools (Ruff, Pyrefly, Biome,
|
|
12
|
+
runs relevant tools (Ruff, Pyrefly, Biome, gofmt, goimports, shfmt,
|
|
13
|
+
shellcheck, and rustfmt) for your project.
|
|
27
14
|
- **Predictable Ownership**: Every supported format has fixed tools, so
|
|
28
15
|
configuration customizes checks without changing which formatter runs.
|
|
29
16
|
- **Style Guides**: A `guide` command that fetches the latest Google style
|
|
@@ -98,7 +85,7 @@ readability check . --unsafe
|
|
|
98
85
|
Fixes that may change a program's behavior or drop its comments are opt-in
|
|
99
86
|
through `--unsafe`, which every tool applies under its own name: Ruff's
|
|
100
87
|
`--unsafe-fixes` and Biome's `--unsafe`. Tools drawing no such distinction,
|
|
101
|
-
such as gofmt, are unaffected by it.
|
|
88
|
+
such as gofmt and goimports, are unaffected by it.
|
|
102
89
|
|
|
103
90
|
### Supported Formats
|
|
104
91
|
|
|
@@ -106,7 +93,9 @@ such as gofmt, are unaffected by it.
|
|
|
106
93
|
| ---------------------------------------------------------------- | --------------------------------------- | ------------------------------------- |
|
|
107
94
|
| `.py` | Ruff lint/format; Pyrefly type checking | Ruff and Pyrefly native configuration |
|
|
108
95
|
| `.js`, `.jsx`, `.ts`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` | Biome | `biome.json` or `biome.jsonc` |
|
|
109
|
-
| `.go` | gofmt
|
|
96
|
+
| `.go` | gofmt, goimports | None |
|
|
97
|
+
| `.sh`, `.bash` | shfmt format; shellcheck lint | None |
|
|
98
|
+
| `.rs` | rustfmt | None |
|
|
110
99
|
|
|
111
100
|
Markdown, YAML, SCSS, JSONL, and extensions not listed above are unsupported.
|
|
112
101
|
An unsupported-only path reports that nothing was checked and exits
|
|
@@ -114,8 +103,8 @@ successfully.
|
|
|
114
103
|
|
|
115
104
|
Biome first checks a project's `node_modules/.bin`. Ruff, Pyrefly, and Biome
|
|
116
105
|
then use `PATH`, followed by `uvx` for the Python tools or `npx` for Biome. The
|
|
117
|
-
runners cache downloads, so subsequent runs work offline. Gofmt
|
|
118
|
-
|
|
106
|
+
runners cache downloads, so subsequent runs work offline. Gofmt and goimports
|
|
107
|
+
must be available on `PATH`.
|
|
119
108
|
|
|
120
109
|
That keeps this package at ~4 MB rather than the ~54 MB it would take to carry
|
|
121
110
|
Ruff and Pyrefly itself — a cost that would fall on everyone using only `guide`.
|
|
@@ -133,8 +122,8 @@ Set `UV_OFFLINE=1` to forbid fetching. A tool that then cannot be reached fails
|
|
|
133
122
|
the run rather than passing it.
|
|
134
123
|
|
|
135
124
|
Ruff, Pyrefly, and Biome ship with bundled configurations, so they run on every
|
|
136
|
-
file they own without project setup. Gofmt
|
|
137
|
-
is no `go.mod`.
|
|
125
|
+
file they own without project setup. Gofmt and goimports run on `.go` files even
|
|
126
|
+
when there is no `go.mod`.
|
|
138
127
|
|
|
139
128
|
A tool that could not be reached at all is never skipped quietly:
|
|
140
129
|
|
|
@@ -189,8 +178,9 @@ caller happened to be standing in.
|
|
|
189
178
|
Ruff and Pyrefly defaults follow the
|
|
190
179
|
[Google Python style guide](https://google.github.io/styleguide/pyguide.html):
|
|
191
180
|
80-column lines, Google docstrings, one import per line sorted as isort's
|
|
192
|
-
`google` profile does,
|
|
193
|
-
|
|
181
|
+
`google` profile does, best-practice lint rules (bugbear, pyupgrade, simplify,
|
|
182
|
+
comprehensions, and Ruff-specific rules), and full type checking. The Biome
|
|
183
|
+
default applies the 80-column lines and two-space indentation of the
|
|
194
184
|
[Google JavaScript style guide](https://google.github.io/styleguide/jsguide.html)
|
|
195
185
|
and enables Biome's recommended lint rules. Project `biome.json` and
|
|
196
186
|
`biome.jsonc` files replace those bundled defaults for Biome-owned formats.
|
|
@@ -199,9 +189,9 @@ For Python, add `[tool.ruff]` or `[tool.pyrefly]` to `pyproject.toml`, or use
|
|
|
199
189
|
`ruff.toml`, `.ruff.toml`, or `pyrefly.toml`. Ruff, Pyrefly, and Biome load
|
|
200
190
|
their native project configurations in place of bundled defaults. Readability
|
|
201
191
|
does not interpret EditorConfig itself; a canonical tool such as Biome may opt
|
|
202
|
-
into it through that tool's native configuration.
|
|
203
|
-
The bundled Biome file requires Biome 2.5 or later,
|
|
204
|
-
runner's version floor.
|
|
192
|
+
into it through that tool's native configuration. Neither gofmt nor goimports
|
|
193
|
+
has project settings. The bundled Biome file requires Biome 2.5 or later,
|
|
194
|
+
matching the fallback runner's version floor.
|
|
205
195
|
|
|
206
196
|
Configuration is found per file, in the file's own directory and then its
|
|
207
197
|
ancestors, which is where each canonical tool looks. A package keeping its
|
|
@@ -368,9 +358,13 @@ Supported languages and their aliases:
|
|
|
368
358
|
- vim [cached]
|
|
369
359
|
```
|
|
370
360
|
|
|
371
|
-
Refresh the cache with `sync`, which takes languages or refetches everything
|
|
361
|
+
Refresh the cache with `sync`, which takes languages or refetches everything.
|
|
362
|
+
It fetches and converts HTML, so it needs the `sync` extra; the bundled guides
|
|
363
|
+
are already converted, and every other command reads those:
|
|
372
364
|
|
|
373
365
|
```bash
|
|
366
|
+
pip install 'readability-cli[sync]'
|
|
367
|
+
|
|
374
368
|
# Refetch one guide, or a few
|
|
375
369
|
readability sync python shell
|
|
376
370
|
|
|
@@ -385,8 +379,8 @@ The tool stores local copies of the style guides in the
|
|
|
385
379
|
the `guide` command uses these local files when they exist. The bundled copies
|
|
386
380
|
are automatically synchronized weekly from the official
|
|
387
381
|
[Google Style Guides](https://google.github.io/styleguide/) repository via
|
|
388
|
-
GitHub Actions,
|
|
389
|
-
command.
|
|
382
|
+
GitHub Actions, so upgrading the package is usually enough. To refresh a local
|
|
383
|
+
cache yourself, install the `sync` extra and run the `sync` command.
|
|
390
384
|
|
|
391
385
|
You can override the default guide directory by setting the
|
|
392
386
|
`READABILITY_CACHE` environment variable. This is useful if you want to store
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "readability-cli"
|
|
3
|
-
version = "0.10.
|
|
3
|
+
version = "0.10.4"
|
|
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"
|
|
7
7
|
dependencies = [
|
|
8
|
-
"beautifulsoup4>=4.14.3",
|
|
9
8
|
"click>=8.3.1",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
# Only `sync` fetches and converts guides; the rest reads bundled copies.
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
sync = [
|
|
14
|
+
"beautifulsoup4>=4.14.3",
|
|
10
15
|
"markdownify>=1.2.2",
|
|
11
16
|
"requests>=2.33.0",
|
|
12
17
|
]
|
|
@@ -34,6 +39,8 @@ testpaths = ["."]
|
|
|
34
39
|
[dependency-groups]
|
|
35
40
|
dev = [
|
|
36
41
|
"pytest>=9.0.2",
|
|
42
|
+
# The suite and the weekly guide refresh both exercise `sync`.
|
|
43
|
+
"readability-cli[sync]",
|
|
37
44
|
"ruff>=0.15.8",
|
|
38
45
|
]
|
|
39
46
|
|
|
@@ -41,7 +48,7 @@ dev = [
|
|
|
41
48
|
line-length = 80
|
|
42
49
|
|
|
43
50
|
[tool.ruff.lint]
|
|
44
|
-
select = ["E", "W", "F", "I", "N", "D", "PL"]
|
|
51
|
+
select = ["E", "W", "F", "I", "N", "D", "PL", "B", "UP", "SIM", "C4", "RUF"]
|
|
45
52
|
ignore = ["PLR0911", "PLR0912", "PLR0913", "PLR0915", "PLR2004"]
|
|
46
53
|
|
|
47
54
|
[tool.ruff.lint.pydocstyle]
|
|
@@ -204,9 +204,10 @@ def _run_tool(
|
|
|
204
204
|
result = _capture_tool_command(command, cwd=tool.cwd)
|
|
205
205
|
if _tool_checked_files(tool.name, result):
|
|
206
206
|
report.ran.add(tool.name)
|
|
207
|
-
# gofmt
|
|
207
|
+
# gofmt and goimports name files rather than using exit codes
|
|
208
208
|
if result.returncode != 0 or (
|
|
209
|
-
tool.name
|
|
209
|
+
tool.name in ("gofmt", "goimports")
|
|
210
|
+
and result.stdout.strip()
|
|
210
211
|
):
|
|
211
212
|
report.findings = True
|
|
212
213
|
click.echo(
|
|
@@ -12,6 +12,7 @@ from readability.guide import get_guide
|
|
|
12
12
|
from readability.guide import get_guides_dir
|
|
13
13
|
from readability.guide import get_local_path
|
|
14
14
|
from readability.guide import LANGUAGE_MAP
|
|
15
|
+
from readability.guide import load_sync_deps
|
|
15
16
|
from readability.guide import refresh_guide
|
|
16
17
|
from readability.outline import _echo_outline
|
|
17
18
|
from readability.outline import _select_section
|
|
@@ -123,6 +124,9 @@ def sync(languages: Sequence[str], verbose: bool) -> None:
|
|
|
123
124
|
if verbose:
|
|
124
125
|
logger.setLevel(logging.DEBUG)
|
|
125
126
|
|
|
127
|
+
# A precondition, not a per-guide failure the loop should report N times
|
|
128
|
+
load_sync_deps()
|
|
129
|
+
|
|
126
130
|
filenames = _resolve_filenames(languages)
|
|
127
131
|
click.echo(f"Synchronizing {len(filenames)} style guide(s)...", err=True)
|
|
128
132
|
|
|
@@ -147,6 +151,10 @@ def sync(languages: Sequence[str], verbose: bool) -> None:
|
|
|
147
151
|
err=True,
|
|
148
152
|
)
|
|
149
153
|
|
|
154
|
+
# A guide that did not refetch leaves a stale local copy behind
|
|
155
|
+
if failure_count:
|
|
156
|
+
sys.exit(1)
|
|
157
|
+
|
|
150
158
|
|
|
151
159
|
def _echo_languages() -> None:
|
|
152
160
|
"""Print the languages that have a guide, with their aliases."""
|
|
@@ -240,7 +248,6 @@ def check(paths: Sequence[str], fix: bool, unsafe: bool, verbose: bool) -> None:
|
|
|
240
248
|
)
|
|
241
249
|
|
|
242
250
|
|
|
243
|
-
# Main entry point for the CLI
|
|
244
251
|
def main() -> None:
|
|
245
252
|
"""Main entry point for the CLI."""
|
|
246
253
|
# Configured here, not at import, so library use has no side effects
|
|
@@ -6,8 +6,9 @@ line-length = 80
|
|
|
6
6
|
|
|
7
7
|
[lint]
|
|
8
8
|
# Pycodestyle (E, W), Pyflakes (F), import order (I), naming (N),
|
|
9
|
-
# docstrings (D),
|
|
10
|
-
|
|
9
|
+
# docstrings (D), pylint (PL), bugbear (B), pyupgrade (UP),
|
|
10
|
+
# simplify (SIM), comprehensions (C4), and ruff-specific (RUF) rules
|
|
11
|
+
select = ["E", "W", "F", "I", "N", "D", "PL", "B", "UP", "SIM", "C4", "RUF"]
|
|
11
12
|
# Complexity counters and magic-value warnings are guidance, not errors
|
|
12
13
|
ignore = ["PLR0911", "PLR0912", "PLR0913", "PLR0915", "PLR2004"]
|
|
13
14
|
|
|
@@ -1,21 +1,46 @@
|
|
|
1
1
|
"""Fetch and cache Google style guides."""
|
|
2
2
|
|
|
3
|
+
import functools
|
|
3
4
|
from importlib.resources import files
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
7
|
+
import types
|
|
6
8
|
import warnings
|
|
7
9
|
|
|
8
|
-
from bs4 import BeautifulSoup
|
|
9
|
-
from bs4 import XMLParsedAsHTMLWarning
|
|
10
10
|
import click
|
|
11
|
-
from markdownify import markdownify as md
|
|
12
|
-
import requests
|
|
13
|
-
|
|
14
|
-
warnings.filterwarnings("ignore", category=XMLParsedAsHTMLWarning)
|
|
15
11
|
|
|
16
12
|
logger = logging.getLogger("readability")
|
|
17
13
|
|
|
18
14
|
|
|
15
|
+
@functools.cache
|
|
16
|
+
def load_sync_deps() -> tuple[
|
|
17
|
+
types.ModuleType, types.ModuleType, types.ModuleType
|
|
18
|
+
]:
|
|
19
|
+
"""Imports the libraries only the guide refresh needs.
|
|
20
|
+
|
|
21
|
+
Guides ship pre-converted, so this stack stays out of the default
|
|
22
|
+
install rather than charging every user for `sync`.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The requests, bs4, and markdownify modules.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
click.ClickException: If the sync extra is not installed.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
import bs4 # noqa: PLC0415
|
|
32
|
+
import markdownify # noqa: PLC0415
|
|
33
|
+
import requests # noqa: PLC0415
|
|
34
|
+
except ImportError as e:
|
|
35
|
+
raise click.ClickException(
|
|
36
|
+
"Refreshing style guides needs extra libraries. Install them "
|
|
37
|
+
"with: pip install 'readability-cli[sync]'"
|
|
38
|
+
) from e
|
|
39
|
+
|
|
40
|
+
warnings.filterwarnings("ignore", category=bs4.XMLParsedAsHTMLWarning)
|
|
41
|
+
return requests, bs4, markdownify
|
|
42
|
+
|
|
43
|
+
|
|
19
44
|
def get_guides_dir() -> str:
|
|
20
45
|
"""Get the directory where style guides are cached.
|
|
21
46
|
|
|
@@ -29,7 +54,6 @@ def get_guides_dir() -> str:
|
|
|
29
54
|
return os.getenv("READABILITY_CACHE") or str(bundled_guides)
|
|
30
55
|
|
|
31
56
|
|
|
32
|
-
# Mapping of languages to their Google Style Guide file paths
|
|
33
57
|
LANGUAGE_MAP = {
|
|
34
58
|
"python": "pyguide.md",
|
|
35
59
|
"shell": "shellguide.md",
|
|
@@ -72,9 +96,9 @@ def get_guide_content(url: str) -> str:
|
|
|
72
96
|
Raises:
|
|
73
97
|
click.ClickException: If the HTTP request fails.
|
|
74
98
|
"""
|
|
99
|
+
requests, _, _ = load_sync_deps()
|
|
75
100
|
logger.info("Fetching style guide from %s", url)
|
|
76
101
|
|
|
77
|
-
# Perform the HTTP GET request with a timeout
|
|
78
102
|
try:
|
|
79
103
|
response = requests.get(url, timeout=10)
|
|
80
104
|
response.raise_for_status()
|
|
@@ -82,7 +106,7 @@ def get_guide_content(url: str) -> str:
|
|
|
82
106
|
logger.error("Failed to fetch content from %s: %s", url, e)
|
|
83
107
|
raise click.ClickException(
|
|
84
108
|
f"Failed to fetch style guide from {url}: {e}"
|
|
85
|
-
)
|
|
109
|
+
) from e
|
|
86
110
|
|
|
87
111
|
return response.text
|
|
88
112
|
|
|
@@ -97,9 +121,9 @@ def convert_to_markdown(content: str, filename: str) -> str:
|
|
|
97
121
|
Returns:
|
|
98
122
|
The converted markdown content.
|
|
99
123
|
"""
|
|
124
|
+
_, bs4, markdownify = load_sync_deps()
|
|
100
125
|
logger.debug("Converting content for %s", filename)
|
|
101
126
|
|
|
102
|
-
# Handle Markdown files directly
|
|
103
127
|
if filename.endswith(".md"):
|
|
104
128
|
return content
|
|
105
129
|
|
|
@@ -107,11 +131,10 @@ def convert_to_markdown(content: str, filename: str) -> str:
|
|
|
107
131
|
if content.lstrip().startswith("<?xml"):
|
|
108
132
|
content = content.split("?>", 1)[-1].lstrip()
|
|
109
133
|
|
|
110
|
-
#
|
|
134
|
+
# The Vim script and JSON guides are only published as XML
|
|
111
135
|
if filename.endswith(".xml"):
|
|
112
|
-
soup = BeautifulSoup(content, "html.parser")
|
|
136
|
+
soup = bs4.BeautifulSoup(content, "html.parser")
|
|
113
137
|
|
|
114
|
-
# Add titles as headers
|
|
115
138
|
guide = soup.find("guide")
|
|
116
139
|
if guide:
|
|
117
140
|
title = guide.get("title")
|
|
@@ -136,11 +159,10 @@ def convert_to_markdown(content: str, filename: str) -> str:
|
|
|
136
159
|
|
|
137
160
|
for summary in soup.find_all("summary"):
|
|
138
161
|
summary.name = "p"
|
|
139
|
-
# Wrap content in strong tags
|
|
140
162
|
content_str = summary.decode_contents()
|
|
141
163
|
summary.clear()
|
|
142
164
|
strong = soup.new_tag("strong")
|
|
143
|
-
strong.append(BeautifulSoup(content_str, "html.parser"))
|
|
165
|
+
strong.append(bs4.BeautifulSoup(content_str, "html.parser"))
|
|
144
166
|
summary.append(strong)
|
|
145
167
|
|
|
146
168
|
for snippet in soup.find_all(["code_snippet", "bad_code_snippet"]):
|
|
@@ -157,14 +179,11 @@ def convert_to_markdown(content: str, filename: str) -> str:
|
|
|
157
179
|
snippet.append(p)
|
|
158
180
|
snippet.append(code)
|
|
159
181
|
|
|
160
|
-
|
|
161
|
-
return md(str(soup), **MARKDOWNIFY_OPTIONS)
|
|
182
|
+
return markdownify.markdownify(str(soup), **MARKDOWNIFY_OPTIONS)
|
|
162
183
|
|
|
163
|
-
# Handle HTML files by converting them to Markdown
|
|
164
184
|
if filename.endswith(".html"):
|
|
165
|
-
return
|
|
185
|
+
return markdownify.markdownify(content, **MARKDOWNIFY_OPTIONS)
|
|
166
186
|
|
|
167
|
-
# Fallback to returning raw content
|
|
168
187
|
return content
|
|
169
188
|
|
|
170
189
|
|
|
@@ -181,9 +200,7 @@ def get_local_path(filename: str) -> str:
|
|
|
181
200
|
Returns:
|
|
182
201
|
The full local path to the cached markdown file.
|
|
183
202
|
"""
|
|
184
|
-
# Flatten the filename by replacing '/' with '-'
|
|
185
203
|
flattened = filename.replace("/", "-")
|
|
186
|
-
# Use the flattened filename and change extension to .md for uniform storage
|
|
187
204
|
base_name = flattened.rsplit(".", 1)[0]
|
|
188
205
|
return os.path.join(get_guides_dir(), f"{base_name}.md")
|
|
189
206
|
|
|
@@ -227,7 +244,6 @@ def get_guide(language: str, remote: bool = False) -> str:
|
|
|
227
244
|
Raises:
|
|
228
245
|
click.UsageError: If the language is not supported.
|
|
229
246
|
"""
|
|
230
|
-
# Look up the filename in the mapping
|
|
231
247
|
filename = LANGUAGE_MAP.get(language.lower())
|
|
232
248
|
if not filename:
|
|
233
249
|
error_msg = f"Language '{language}' is not supported."
|
|
@@ -239,10 +255,9 @@ def get_guide(language: str, remote: bool = False) -> str:
|
|
|
239
255
|
|
|
240
256
|
local_path = get_local_path(filename)
|
|
241
257
|
|
|
242
|
-
# If remote is False, check for local file first
|
|
243
258
|
if not remote and os.path.exists(local_path):
|
|
244
259
|
logger.info("Reading style guide from local file: %s", local_path)
|
|
245
|
-
with open(local_path,
|
|
260
|
+
with open(local_path, encoding="utf-8") as f:
|
|
246
261
|
return f.read()
|
|
247
262
|
|
|
248
263
|
return refresh_guide(filename)
|
|
@@ -347,6 +347,10 @@ TOOL_EXTENSIONS = {
|
|
|
347
347
|
".html",
|
|
348
348
|
),
|
|
349
349
|
"gofmt": (".go",),
|
|
350
|
+
"goimports": (".go",),
|
|
351
|
+
"shfmt": (".sh", ".bash"),
|
|
352
|
+
"shellcheck": (".sh", ".bash"),
|
|
353
|
+
"rustfmt": (".rs",),
|
|
350
354
|
}
|
|
351
355
|
|
|
352
356
|
# Headroom below common argv limits; long target lists are split before exec
|
|
@@ -354,8 +358,20 @@ MAX_COMMAND_BYTES = 16 * 1024
|
|
|
354
358
|
|
|
355
359
|
# Named files bypass a tool's own exclusions, and Pyrefly ignores its excludes
|
|
356
360
|
PRUNED_DIRECTORIES = frozenset(
|
|
357
|
-
|
|
358
|
-
|
|
361
|
+
[
|
|
362
|
+
".git",
|
|
363
|
+
".hg",
|
|
364
|
+
".svn",
|
|
365
|
+
".venv",
|
|
366
|
+
"venv",
|
|
367
|
+
".tox",
|
|
368
|
+
".nox",
|
|
369
|
+
"node_modules",
|
|
370
|
+
"__pycache__",
|
|
371
|
+
".mypy_cache",
|
|
372
|
+
".pytest_cache",
|
|
373
|
+
".ruff_cache",
|
|
374
|
+
]
|
|
359
375
|
)
|
|
360
376
|
|
|
361
377
|
# Per tool: config filenames, pyproject section (None if it has none), flag
|
|
@@ -392,6 +408,13 @@ TOOL_PHASES = {
|
|
|
392
408
|
"format": ("format", "--write", "--no-errors-on-unmatched"),
|
|
393
409
|
},
|
|
394
410
|
"gofmt": {"check_format": ("-l",), "format": ("-w",)},
|
|
411
|
+
"goimports": {"check_format": ("-l",), "format": ("-w",)},
|
|
412
|
+
"shfmt": {
|
|
413
|
+
"check_format": ("-i", "2", "-ci", "-d"),
|
|
414
|
+
"format": ("-i", "2", "-ci", "-w"),
|
|
415
|
+
},
|
|
416
|
+
"shellcheck": {"check": ()},
|
|
417
|
+
"rustfmt": {"check_format": ("--check",), "format": ()},
|
|
395
418
|
}
|
|
396
419
|
|
|
397
420
|
# Per tool: how it names fixes that may change behavior or drop comments
|
|
@@ -549,6 +572,42 @@ def _get_tool_definitions(
|
|
|
549
572
|
path,
|
|
550
573
|
)
|
|
551
574
|
)
|
|
575
|
+
plans.append(
|
|
576
|
+
_plan(
|
|
577
|
+
"goimports",
|
|
578
|
+
("goimports",),
|
|
579
|
+
[],
|
|
580
|
+
_matching_paths(path, TOOL_EXTENSIONS["goimports"]),
|
|
581
|
+
path,
|
|
582
|
+
)
|
|
583
|
+
)
|
|
584
|
+
plans.append(
|
|
585
|
+
_plan(
|
|
586
|
+
"shfmt",
|
|
587
|
+
("shfmt",),
|
|
588
|
+
[],
|
|
589
|
+
_matching_paths(path, TOOL_EXTENSIONS["shfmt"]),
|
|
590
|
+
path,
|
|
591
|
+
)
|
|
592
|
+
)
|
|
593
|
+
plans.append(
|
|
594
|
+
_plan(
|
|
595
|
+
"shellcheck",
|
|
596
|
+
("shellcheck",),
|
|
597
|
+
[],
|
|
598
|
+
_matching_paths(path, TOOL_EXTENSIONS["shellcheck"]),
|
|
599
|
+
path,
|
|
600
|
+
)
|
|
601
|
+
)
|
|
602
|
+
plans.append(
|
|
603
|
+
_plan(
|
|
604
|
+
"rustfmt",
|
|
605
|
+
("rustfmt",),
|
|
606
|
+
[],
|
|
607
|
+
_matching_paths(path, TOOL_EXTENSIONS["rustfmt"]),
|
|
608
|
+
path,
|
|
609
|
+
)
|
|
610
|
+
)
|
|
552
611
|
|
|
553
612
|
# A directory owning its config alone lets Pyrefly select the files
|
|
554
613
|
files, section = CONFIG_SOURCES["pyrefly"][:2]
|
|
@@ -15,6 +15,8 @@ from readability.cli import cli
|
|
|
15
15
|
from readability.tools import _bundled_config
|
|
16
16
|
from readability.tools import _get_tool_definitions
|
|
17
17
|
from readability.tools import _has_project_config
|
|
18
|
+
from readability.tools import TOOL_EXTENSIONS
|
|
19
|
+
from readability.tools import TOOL_PHASES
|
|
18
20
|
from readability.tools import TOOL_RUNNERS
|
|
19
21
|
|
|
20
22
|
|
|
@@ -1045,6 +1047,17 @@ def test_bundled_default_configs_are_valid() -> None:
|
|
|
1045
1047
|
}
|
|
1046
1048
|
|
|
1047
1049
|
|
|
1050
|
+
def test_bundled_ruff_config_includes_enriched_rules() -> None:
|
|
1051
|
+
"""The default ruff config selects best-practice and modernization rules."""
|
|
1052
|
+
# Enriched rules for bug prevention, modern syntax, and simplification
|
|
1053
|
+
ruff_config = tomllib.loads(_bundled_config("ruff").read_text())
|
|
1054
|
+
selected_rules = set(ruff_config["lint"]["select"])
|
|
1055
|
+
expected_rules = {"B", "UP", "SIM", "C4", "RUF"}
|
|
1056
|
+
|
|
1057
|
+
# All enriched rule sets must be present
|
|
1058
|
+
assert expected_rules.issubset(selected_rules)
|
|
1059
|
+
|
|
1060
|
+
|
|
1048
1061
|
@patch("readability.checking._check_path")
|
|
1049
1062
|
def test_check_paths_aggregates_str_and_path_inputs(
|
|
1050
1063
|
mock_check_path: MagicMock,
|
|
@@ -1608,6 +1621,170 @@ def test_gofmt_fix_failure_is_reported(
|
|
|
1608
1621
|
assert "gofmt formatting findings" in result.output
|
|
1609
1622
|
|
|
1610
1623
|
|
|
1624
|
+
def test_goimports_tool_plan_generation(tmp_path: Path) -> None:
|
|
1625
|
+
"""Go files produce goimports plans alongside gofmt."""
|
|
1626
|
+
go_file = tmp_path / "main.go"
|
|
1627
|
+
go_file.touch()
|
|
1628
|
+
|
|
1629
|
+
# Verify tool registration in extensions and phases
|
|
1630
|
+
assert TOOL_EXTENSIONS["goimports"] == (".go",)
|
|
1631
|
+
assert TOOL_PHASES["goimports"] == {
|
|
1632
|
+
"check_format": ("-l",),
|
|
1633
|
+
"format": ("-w",),
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
# Verify tool plan creation
|
|
1637
|
+
plans = {
|
|
1638
|
+
plan.name: plan for plan in _get_tool_definitions(go_file, tmp_path)
|
|
1639
|
+
}
|
|
1640
|
+
assert "goimports" in plans
|
|
1641
|
+
goimports = plans["goimports"]
|
|
1642
|
+
assert goimports.extensions == (".go",)
|
|
1643
|
+
assert goimports.check_format == ("goimports", "-l", str(go_file))
|
|
1644
|
+
assert goimports.format == ("goimports", "-w", str(go_file))
|
|
1645
|
+
assert goimports.check == ()
|
|
1646
|
+
assert goimports.fix == ()
|
|
1647
|
+
assert goimports.targets == (str(go_file),)
|
|
1648
|
+
|
|
1649
|
+
|
|
1650
|
+
@pytest.mark.parametrize("fix", (False, True))
|
|
1651
|
+
@patch("shutil.which")
|
|
1652
|
+
@patch("subprocess.run")
|
|
1653
|
+
def test_configless_go_uses_goimports_for_check_and_fix(
|
|
1654
|
+
mock_run: MagicMock,
|
|
1655
|
+
mock_which: MagicMock,
|
|
1656
|
+
tmp_path: Path,
|
|
1657
|
+
fix: bool,
|
|
1658
|
+
) -> None:
|
|
1659
|
+
"""Standalone Go files use symmetric goimports commands without go.mod."""
|
|
1660
|
+
mock_which.side_effect = lambda name: name if name == "goimports" else None
|
|
1661
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
1662
|
+
|
|
1663
|
+
# Run check or format on Go file
|
|
1664
|
+
runner = CliRunner()
|
|
1665
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1666
|
+
Path("main.go").touch()
|
|
1667
|
+
arguments = ["check", "main.go"]
|
|
1668
|
+
if fix:
|
|
1669
|
+
arguments.append("--fix")
|
|
1670
|
+
|
|
1671
|
+
result = runner.invoke(cli, arguments)
|
|
1672
|
+
|
|
1673
|
+
# Verify goimports command invoked with correct flags
|
|
1674
|
+
assert result.exit_code == 0
|
|
1675
|
+
expected = ["goimports", "-w" if fix else "-l", "main.go"]
|
|
1676
|
+
assert [invocation.args[0] for invocation in mock_run.call_args_list] == [
|
|
1677
|
+
expected
|
|
1678
|
+
]
|
|
1679
|
+
|
|
1680
|
+
|
|
1681
|
+
@patch("shutil.which")
|
|
1682
|
+
@patch("subprocess.run")
|
|
1683
|
+
def test_goimports_scopes_a_directory_to_go_files(
|
|
1684
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1685
|
+
) -> None:
|
|
1686
|
+
"""Goimports receives recursive Go files instead of an invalid directory."""
|
|
1687
|
+
mock_which.side_effect = lambda name: name if name == "goimports" else None
|
|
1688
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
1689
|
+
|
|
1690
|
+
# Populate directory structure with mixed files
|
|
1691
|
+
runner = CliRunner()
|
|
1692
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1693
|
+
Path("src/nested").mkdir(parents=True)
|
|
1694
|
+
Path("src/main.go").touch()
|
|
1695
|
+
Path("src/nested/helper.go").touch()
|
|
1696
|
+
Path("src/notes.txt").touch()
|
|
1697
|
+
|
|
1698
|
+
result = runner.invoke(cli, ["check", "src"])
|
|
1699
|
+
|
|
1700
|
+
# Verify only Go files are passed to goimports
|
|
1701
|
+
assert result.exit_code == 0
|
|
1702
|
+
assert [invocation.args[0] for invocation in mock_run.call_args_list] == [
|
|
1703
|
+
["goimports", "-l", "src/main.go", "src/nested/helper.go"]
|
|
1704
|
+
]
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
@patch("shutil.which")
|
|
1708
|
+
@patch("subprocess.run")
|
|
1709
|
+
def test_goimports_check_reports_diff_in_stdout_as_findings(
|
|
1710
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1711
|
+
) -> None:
|
|
1712
|
+
"""Goimports reports diffs on stdout while exiting zero."""
|
|
1713
|
+
mock_which.side_effect = lambda name: name if name == "goimports" else None
|
|
1714
|
+
mock_run.return_value = MagicMock(
|
|
1715
|
+
returncode=0, stdout="main.go\n", stderr=""
|
|
1716
|
+
)
|
|
1717
|
+
|
|
1718
|
+
# Invoke check on unformatted Go file
|
|
1719
|
+
runner = CliRunner()
|
|
1720
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1721
|
+
Path("main.go").touch()
|
|
1722
|
+
|
|
1723
|
+
result = runner.invoke(cli, ["check", "main.go"])
|
|
1724
|
+
|
|
1725
|
+
# Findings in stdout must fail the check
|
|
1726
|
+
assert result.exit_code == 1
|
|
1727
|
+
assert "--- goimports formatting findings ---" in result.output
|
|
1728
|
+
assert "main.go" in result.output
|
|
1729
|
+
|
|
1730
|
+
|
|
1731
|
+
@patch("shutil.which")
|
|
1732
|
+
@patch("subprocess.run")
|
|
1733
|
+
def test_goimports_fix_failure_is_reported(
|
|
1734
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1735
|
+
) -> None:
|
|
1736
|
+
"""A failed goimports write cannot be reported as a clean fix."""
|
|
1737
|
+
mock_which.side_effect = lambda name: name if name == "goimports" else None
|
|
1738
|
+
mock_run.return_value = MagicMock(
|
|
1739
|
+
returncode=1, stdout="", stderr="invalid Go syntax"
|
|
1740
|
+
)
|
|
1741
|
+
|
|
1742
|
+
# Invoke check --fix on invalid Go file
|
|
1743
|
+
runner = CliRunner()
|
|
1744
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1745
|
+
Path("main.go").touch()
|
|
1746
|
+
|
|
1747
|
+
result = runner.invoke(cli, ["check", "main.go", "--fix"])
|
|
1748
|
+
|
|
1749
|
+
# Fix error must fail the check
|
|
1750
|
+
assert result.exit_code == 1
|
|
1751
|
+
assert "goimports formatting findings" in result.output
|
|
1752
|
+
|
|
1753
|
+
|
|
1754
|
+
@pytest.mark.parametrize("fix", (False, True))
|
|
1755
|
+
@patch("shutil.which")
|
|
1756
|
+
@patch("subprocess.run")
|
|
1757
|
+
def test_go_runs_both_gofmt_and_goimports(
|
|
1758
|
+
mock_run: MagicMock,
|
|
1759
|
+
mock_which: MagicMock,
|
|
1760
|
+
tmp_path: Path,
|
|
1761
|
+
fix: bool,
|
|
1762
|
+
) -> None:
|
|
1763
|
+
"""When both gofmt and goimports are installed, both run on Go files."""
|
|
1764
|
+
mock_which.side_effect = lambda name: (
|
|
1765
|
+
name if name in ("gofmt", "goimports") else None
|
|
1766
|
+
)
|
|
1767
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
1768
|
+
|
|
1769
|
+
# Run check or fix with both Go tools available
|
|
1770
|
+
runner = CliRunner()
|
|
1771
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1772
|
+
Path("main.go").touch()
|
|
1773
|
+
arguments = ["check", "main.go"]
|
|
1774
|
+
if fix:
|
|
1775
|
+
arguments.append("--fix")
|
|
1776
|
+
|
|
1777
|
+
result = runner.invoke(cli, arguments)
|
|
1778
|
+
|
|
1779
|
+
# Verify both tools ran with the appropriate flag
|
|
1780
|
+
assert result.exit_code == 0
|
|
1781
|
+
flag = "-w" if fix else "-l"
|
|
1782
|
+
assert [invocation.args[0] for invocation in mock_run.call_args_list] == [
|
|
1783
|
+
["gofmt", flag, "main.go"],
|
|
1784
|
+
["goimports", flag, "main.go"],
|
|
1785
|
+
]
|
|
1786
|
+
|
|
1787
|
+
|
|
1611
1788
|
@patch("shutil.which")
|
|
1612
1789
|
@patch("subprocess.run")
|
|
1613
1790
|
def test_large_biome_directory_uses_bounded_commands(
|
|
@@ -1653,6 +1830,7 @@ def test_missing_gofmt_is_reported_for_configless_go(
|
|
|
1653
1830
|
|
|
1654
1831
|
assert result.exit_code == 1
|
|
1655
1832
|
assert "gofmt" in result.stderr
|
|
1833
|
+
assert "goimports" in result.stderr
|
|
1656
1834
|
assert "nothing was verified" in result.stderr
|
|
1657
1835
|
|
|
1658
1836
|
|
|
@@ -2195,3 +2373,321 @@ def test_unsafe_is_a_no_op_for_tools_without_one(
|
|
|
2195
2373
|
assert [invocation.args[0] for invocation in mock_run.call_args_list] == [
|
|
2196
2374
|
["gofmt", "-w", "main.go"]
|
|
2197
2375
|
]
|
|
2376
|
+
|
|
2377
|
+
|
|
2378
|
+
@pytest.mark.parametrize("extension", (".sh", ".bash"))
|
|
2379
|
+
def test_tool_definitions_for_shell(extension: str, tmp_path: Path) -> None:
|
|
2380
|
+
"""Shell files yield shfmt and shellcheck plans with style arguments."""
|
|
2381
|
+
# Create the target shell file
|
|
2382
|
+
script = tmp_path / f"script{extension}"
|
|
2383
|
+
script.touch()
|
|
2384
|
+
|
|
2385
|
+
# Resolve tool plans for the file
|
|
2386
|
+
tools = {
|
|
2387
|
+
tool.name: tool for tool in _get_tool_definitions(script, tmp_path)
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
# Verify shfmt plan
|
|
2391
|
+
shfmt = tools["shfmt"]
|
|
2392
|
+
assert shfmt.extensions == (".sh", ".bash")
|
|
2393
|
+
assert shfmt.check_format == ("shfmt", "-i", "2", "-ci", "-d", str(script))
|
|
2394
|
+
assert shfmt.format == ("shfmt", "-i", "2", "-ci", "-w", str(script))
|
|
2395
|
+
assert shfmt.check == ()
|
|
2396
|
+
assert shfmt.fix == ()
|
|
2397
|
+
|
|
2398
|
+
# Verify shellcheck plan
|
|
2399
|
+
shellcheck = tools["shellcheck"]
|
|
2400
|
+
assert shellcheck.extensions == (".sh", ".bash")
|
|
2401
|
+
assert shellcheck.check == ("shellcheck", str(script))
|
|
2402
|
+
assert shellcheck.check_format == ()
|
|
2403
|
+
assert shellcheck.format == ()
|
|
2404
|
+
assert shellcheck.fix == ()
|
|
2405
|
+
|
|
2406
|
+
|
|
2407
|
+
def test_tool_definitions_for_rust(tmp_path: Path) -> None:
|
|
2408
|
+
"""Rust files yield rustfmt plans with check and in-place formatting."""
|
|
2409
|
+
# Create the target Rust file
|
|
2410
|
+
source = tmp_path / "main.rs"
|
|
2411
|
+
source.touch()
|
|
2412
|
+
|
|
2413
|
+
# Resolve tool plans for the file
|
|
2414
|
+
tools = {
|
|
2415
|
+
tool.name: tool for tool in _get_tool_definitions(source, tmp_path)
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
# Verify rustfmt plan
|
|
2419
|
+
rustfmt = tools["rustfmt"]
|
|
2420
|
+
assert rustfmt.extensions == (".rs",)
|
|
2421
|
+
assert rustfmt.check_format == ("rustfmt", "--check", str(source))
|
|
2422
|
+
assert rustfmt.format == ("rustfmt", str(source))
|
|
2423
|
+
assert rustfmt.check == ()
|
|
2424
|
+
assert rustfmt.fix == ()
|
|
2425
|
+
|
|
2426
|
+
|
|
2427
|
+
@pytest.mark.parametrize("extension", (".sh", ".bash"))
|
|
2428
|
+
@pytest.mark.parametrize("fix", (False, True))
|
|
2429
|
+
@patch("shutil.which")
|
|
2430
|
+
@patch("subprocess.run")
|
|
2431
|
+
def test_shell_uses_shfmt_and_shellcheck_for_check_and_fix(
|
|
2432
|
+
mock_run: MagicMock,
|
|
2433
|
+
mock_which: MagicMock,
|
|
2434
|
+
tmp_path: Path,
|
|
2435
|
+
extension: str,
|
|
2436
|
+
fix: bool,
|
|
2437
|
+
) -> None:
|
|
2438
|
+
"""Shell files invoke shfmt and shellcheck during check and fix."""
|
|
2439
|
+
# Allow shfmt and shellcheck to appear installed
|
|
2440
|
+
mock_which.side_effect = lambda name: (
|
|
2441
|
+
name if name in ("shfmt", "shellcheck") else None
|
|
2442
|
+
)
|
|
2443
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2444
|
+
|
|
2445
|
+
# Run check command in an isolated environment
|
|
2446
|
+
runner = CliRunner()
|
|
2447
|
+
filename = f"script{extension}"
|
|
2448
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2449
|
+
Path(filename).touch()
|
|
2450
|
+
arguments = ["check", filename]
|
|
2451
|
+
if fix:
|
|
2452
|
+
arguments.append("--fix")
|
|
2453
|
+
result = runner.invoke(cli, arguments)
|
|
2454
|
+
|
|
2455
|
+
# Verify clean exit and expected tool invocations
|
|
2456
|
+
assert result.exit_code == 0
|
|
2457
|
+
shfmt_action = "-w" if fix else "-d"
|
|
2458
|
+
expected = [
|
|
2459
|
+
["shfmt", "-i", "2", "-ci", shfmt_action, filename],
|
|
2460
|
+
["shellcheck", filename],
|
|
2461
|
+
]
|
|
2462
|
+
assert [
|
|
2463
|
+
invocation.args[0] for invocation in mock_run.call_args_list
|
|
2464
|
+
] == expected
|
|
2465
|
+
|
|
2466
|
+
|
|
2467
|
+
@pytest.mark.parametrize("fix", (False, True))
|
|
2468
|
+
@patch("shutil.which")
|
|
2469
|
+
@patch("subprocess.run")
|
|
2470
|
+
def test_rust_uses_rustfmt_for_check_and_fix(
|
|
2471
|
+
mock_run: MagicMock,
|
|
2472
|
+
mock_which: MagicMock,
|
|
2473
|
+
tmp_path: Path,
|
|
2474
|
+
fix: bool,
|
|
2475
|
+
) -> None:
|
|
2476
|
+
"""Rust files invoke rustfmt with check flag or in-place format."""
|
|
2477
|
+
# Allow rustfmt to appear installed
|
|
2478
|
+
mock_which.side_effect = lambda name: name if name == "rustfmt" else None
|
|
2479
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2480
|
+
|
|
2481
|
+
# Run check command in an isolated environment
|
|
2482
|
+
runner = CliRunner()
|
|
2483
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2484
|
+
Path("main.rs").touch()
|
|
2485
|
+
arguments = ["check", "main.rs"]
|
|
2486
|
+
if fix:
|
|
2487
|
+
arguments.append("--fix")
|
|
2488
|
+
result = runner.invoke(cli, arguments)
|
|
2489
|
+
|
|
2490
|
+
# Verify clean exit and expected tool invocations
|
|
2491
|
+
assert result.exit_code == 0
|
|
2492
|
+
expected = (
|
|
2493
|
+
[["rustfmt", "main.rs"]] if fix else [["rustfmt", "--check", "main.rs"]]
|
|
2494
|
+
)
|
|
2495
|
+
assert [
|
|
2496
|
+
invocation.args[0] for invocation in mock_run.call_args_list
|
|
2497
|
+
] == expected
|
|
2498
|
+
|
|
2499
|
+
|
|
2500
|
+
@patch("shutil.which")
|
|
2501
|
+
@patch("subprocess.run")
|
|
2502
|
+
def test_shell_tools_scope_a_directory_to_shell_files(
|
|
2503
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2504
|
+
) -> None:
|
|
2505
|
+
"""Shell tools only receive shell files under checked directories."""
|
|
2506
|
+
# Allow shfmt and shellcheck to appear installed
|
|
2507
|
+
mock_which.side_effect = lambda name: (
|
|
2508
|
+
name if name in ("shfmt", "shellcheck") else None
|
|
2509
|
+
)
|
|
2510
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2511
|
+
|
|
2512
|
+
# Populate directory with shell and non-shell files
|
|
2513
|
+
runner = CliRunner()
|
|
2514
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2515
|
+
Path("src/nested").mkdir(parents=True)
|
|
2516
|
+
Path("src/script.sh").touch()
|
|
2517
|
+
Path("src/nested/setup.bash").touch()
|
|
2518
|
+
Path("src/notes.txt").touch()
|
|
2519
|
+
result = runner.invoke(cli, ["check", "src"])
|
|
2520
|
+
|
|
2521
|
+
# Verify tool calls are scoped to matching files
|
|
2522
|
+
assert result.exit_code == 0
|
|
2523
|
+
expected_files = ["src/nested/setup.bash", "src/script.sh"]
|
|
2524
|
+
assert [invocation.args[0] for invocation in mock_run.call_args_list] == [
|
|
2525
|
+
["shfmt", "-i", "2", "-ci", "-d", *expected_files],
|
|
2526
|
+
["shellcheck", *expected_files],
|
|
2527
|
+
]
|
|
2528
|
+
|
|
2529
|
+
|
|
2530
|
+
@patch("shutil.which")
|
|
2531
|
+
@patch("subprocess.run")
|
|
2532
|
+
def test_rustfmt_scopes_a_directory_to_rust_files(
|
|
2533
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2534
|
+
) -> None:
|
|
2535
|
+
"""Rustfmt only receives Rust files under checked directories."""
|
|
2536
|
+
# Allow rustfmt to appear installed
|
|
2537
|
+
mock_which.side_effect = lambda name: name if name == "rustfmt" else None
|
|
2538
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2539
|
+
|
|
2540
|
+
# Populate directory with Rust and non-Rust files
|
|
2541
|
+
runner = CliRunner()
|
|
2542
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2543
|
+
Path("src/nested").mkdir(parents=True)
|
|
2544
|
+
Path("src/main.rs").touch()
|
|
2545
|
+
Path("src/nested/lib.rs").touch()
|
|
2546
|
+
Path("src/notes.txt").touch()
|
|
2547
|
+
result = runner.invoke(cli, ["check", "src"])
|
|
2548
|
+
|
|
2549
|
+
# Verify tool calls are scoped to matching files
|
|
2550
|
+
assert result.exit_code == 0
|
|
2551
|
+
assert [invocation.args[0] for invocation in mock_run.call_args_list] == [
|
|
2552
|
+
["rustfmt", "--check", "src/main.rs", "src/nested/lib.rs"]
|
|
2553
|
+
]
|
|
2554
|
+
|
|
2555
|
+
|
|
2556
|
+
@patch("shutil.which")
|
|
2557
|
+
@patch("subprocess.run")
|
|
2558
|
+
def test_shfmt_formatting_findings_reported(
|
|
2559
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2560
|
+
) -> None:
|
|
2561
|
+
"""Non-zero exit from shfmt diff is reported as a formatting finding."""
|
|
2562
|
+
# Allow shfmt to appear installed
|
|
2563
|
+
mock_which.side_effect = lambda name: (
|
|
2564
|
+
name if name in ("shfmt", "shellcheck") else None
|
|
2565
|
+
)
|
|
2566
|
+
mock_run.side_effect = [
|
|
2567
|
+
MagicMock(
|
|
2568
|
+
returncode=1, stdout="--- script.sh\n+++ script.sh\n", stderr=""
|
|
2569
|
+
),
|
|
2570
|
+
MagicMock(returncode=0, stdout="", stderr=""),
|
|
2571
|
+
]
|
|
2572
|
+
|
|
2573
|
+
# Run check command
|
|
2574
|
+
runner = CliRunner()
|
|
2575
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2576
|
+
Path("script.sh").touch()
|
|
2577
|
+
result = runner.invoke(cli, ["check", "script.sh"])
|
|
2578
|
+
|
|
2579
|
+
# Verify exit code and finding output
|
|
2580
|
+
assert result.exit_code == 1
|
|
2581
|
+
assert "shfmt formatting findings" in result.output
|
|
2582
|
+
|
|
2583
|
+
|
|
2584
|
+
@patch("shutil.which")
|
|
2585
|
+
@patch("subprocess.run")
|
|
2586
|
+
def test_shellcheck_findings_reported(
|
|
2587
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2588
|
+
) -> None:
|
|
2589
|
+
"""Non-zero exit from shellcheck is reported as findings."""
|
|
2590
|
+
# Allow shellcheck to appear installed
|
|
2591
|
+
mock_which.side_effect = lambda name: (
|
|
2592
|
+
name if name in ("shfmt", "shellcheck") else None
|
|
2593
|
+
)
|
|
2594
|
+
mock_run.side_effect = [
|
|
2595
|
+
MagicMock(returncode=0, stdout="", stderr=""),
|
|
2596
|
+
MagicMock(
|
|
2597
|
+
returncode=1,
|
|
2598
|
+
stdout="SC2086: Double quote to prevent globbing",
|
|
2599
|
+
stderr="",
|
|
2600
|
+
),
|
|
2601
|
+
]
|
|
2602
|
+
|
|
2603
|
+
# Run check command
|
|
2604
|
+
runner = CliRunner()
|
|
2605
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2606
|
+
Path("script.sh").touch()
|
|
2607
|
+
result = runner.invoke(cli, ["check", "script.sh"])
|
|
2608
|
+
|
|
2609
|
+
# Verify exit code and finding output
|
|
2610
|
+
assert result.exit_code == 1
|
|
2611
|
+
assert "shellcheck findings" in result.output
|
|
2612
|
+
|
|
2613
|
+
|
|
2614
|
+
@patch("shutil.which")
|
|
2615
|
+
@patch("subprocess.run")
|
|
2616
|
+
def test_rustfmt_formatting_findings_reported(
|
|
2617
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2618
|
+
) -> None:
|
|
2619
|
+
"""Non-zero exit from rustfmt check is reported as a formatting finding."""
|
|
2620
|
+
# Allow rustfmt to appear installed
|
|
2621
|
+
mock_which.side_effect = lambda name: name if name == "rustfmt" else None
|
|
2622
|
+
mock_run.return_value = MagicMock(
|
|
2623
|
+
returncode=1, stdout="Diff in main.rs", stderr=""
|
|
2624
|
+
)
|
|
2625
|
+
|
|
2626
|
+
# Run check command
|
|
2627
|
+
runner = CliRunner()
|
|
2628
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2629
|
+
Path("main.rs").touch()
|
|
2630
|
+
result = runner.invoke(cli, ["check", "main.rs"])
|
|
2631
|
+
|
|
2632
|
+
# Verify exit code and finding output
|
|
2633
|
+
assert result.exit_code == 1
|
|
2634
|
+
assert "rustfmt formatting findings" in result.output
|
|
2635
|
+
|
|
2636
|
+
|
|
2637
|
+
@patch("shutil.which", return_value=None)
|
|
2638
|
+
def test_missing_shell_tools_reported(
|
|
2639
|
+
mock_which: MagicMock, tmp_path: Path
|
|
2640
|
+
) -> None:
|
|
2641
|
+
"""Missing shell tools leave shell files unverified."""
|
|
2642
|
+
# Run check command with missing tools
|
|
2643
|
+
runner = CliRunner()
|
|
2644
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2645
|
+
Path("script.sh").touch()
|
|
2646
|
+
result = runner.invoke(cli, ["check", "script.sh"])
|
|
2647
|
+
|
|
2648
|
+
# Verify error message and exit code
|
|
2649
|
+
assert result.exit_code == 1
|
|
2650
|
+
assert "shfmt" in result.stderr
|
|
2651
|
+
assert "shellcheck" in result.stderr
|
|
2652
|
+
assert "nothing was verified" in result.stderr
|
|
2653
|
+
|
|
2654
|
+
|
|
2655
|
+
@patch("shutil.which", return_value=None)
|
|
2656
|
+
def test_missing_rustfmt_reported(
|
|
2657
|
+
mock_which: MagicMock, tmp_path: Path
|
|
2658
|
+
) -> None:
|
|
2659
|
+
"""Missing rustfmt leaves Rust files unverified."""
|
|
2660
|
+
# Run check command with missing rustfmt
|
|
2661
|
+
runner = CliRunner()
|
|
2662
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2663
|
+
Path("main.rs").touch()
|
|
2664
|
+
result = runner.invoke(cli, ["check", "main.rs"])
|
|
2665
|
+
|
|
2666
|
+
# Verify error message and exit code
|
|
2667
|
+
assert result.exit_code == 1
|
|
2668
|
+
assert "rustfmt" in result.stderr
|
|
2669
|
+
assert "nothing was verified" in result.stderr
|
|
2670
|
+
|
|
2671
|
+
|
|
2672
|
+
@patch("shutil.which")
|
|
2673
|
+
@patch("subprocess.run")
|
|
2674
|
+
def test_partial_shell_tools_installed_warns_and_runs_installed(
|
|
2675
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2676
|
+
) -> None:
|
|
2677
|
+
"""A lone installed shell tool runs and warns that the other is absent."""
|
|
2678
|
+
# Only shfmt is installed
|
|
2679
|
+
mock_which.side_effect = lambda name: name if name == "shfmt" else None
|
|
2680
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2681
|
+
|
|
2682
|
+
# Run check command
|
|
2683
|
+
runner = CliRunner()
|
|
2684
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2685
|
+
Path("script.sh").touch()
|
|
2686
|
+
result = runner.invoke(cli, ["check", "script.sh"])
|
|
2687
|
+
|
|
2688
|
+
# Verify shfmt ran, shellcheck was skipped with warning, exit code 0
|
|
2689
|
+
assert result.exit_code == 0
|
|
2690
|
+
assert "shellcheck" in result.stderr
|
|
2691
|
+
assert [invocation.args[0] for invocation in mock_run.call_args_list] == [
|
|
2692
|
+
["shfmt", "-i", "2", "-ci", "-d", "script.sh"]
|
|
2693
|
+
]
|
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from unittest.mock import MagicMock
|
|
5
5
|
from unittest.mock import patch
|
|
6
6
|
|
|
7
|
+
import click
|
|
7
8
|
from click.testing import CliRunner
|
|
8
9
|
|
|
9
10
|
from readability.cli import cli
|
|
@@ -523,7 +524,7 @@ def _shipped_guides() -> list[tuple[str, str]]:
|
|
|
523
524
|
if filename in seen or not os.path.exists(path):
|
|
524
525
|
continue
|
|
525
526
|
seen.add(filename)
|
|
526
|
-
with open(path,
|
|
527
|
+
with open(path, encoding="utf-8") as f:
|
|
527
528
|
guides.append((language, f.read()))
|
|
528
529
|
return guides
|
|
529
530
|
|
|
@@ -787,6 +788,21 @@ def test_sync_deduplicates_aliases(
|
|
|
787
788
|
assert mock_get_content.call_count == 1
|
|
788
789
|
|
|
789
790
|
|
|
791
|
+
@patch("readability.guide.get_guide_content")
|
|
792
|
+
def test_sync_reports_a_failed_fetch_in_its_exit_code(
|
|
793
|
+
mock_get_content: MagicMock, tmp_path: Path
|
|
794
|
+
) -> None:
|
|
795
|
+
"""A stale local copy left behind is a failure, not a quiet success."""
|
|
796
|
+
mock_get_content.side_effect = click.ClickException("network is down")
|
|
797
|
+
|
|
798
|
+
with patch("readability.guide.get_guides_dir", return_value=str(tmp_path)):
|
|
799
|
+
runner = CliRunner()
|
|
800
|
+
result = runner.invoke(cli, ["sync", "python"])
|
|
801
|
+
|
|
802
|
+
assert result.exit_code != 0
|
|
803
|
+
assert "Failures: 1" in result.stderr
|
|
804
|
+
|
|
805
|
+
|
|
790
806
|
def test_sync_rejects_an_unsupported_language(tmp_path: Path) -> None:
|
|
791
807
|
"""An unknown language is a typo, not a reason to sync everything."""
|
|
792
808
|
with patch("readability.guide.get_guides_dir", return_value=str(tmp_path)):
|
|
@@ -194,11 +194,15 @@ wheels = [
|
|
|
194
194
|
|
|
195
195
|
[[package]]
|
|
196
196
|
name = "readability-cli"
|
|
197
|
-
version = "0.10.
|
|
197
|
+
version = "0.10.4"
|
|
198
198
|
source = { editable = "." }
|
|
199
199
|
dependencies = [
|
|
200
|
-
{ name = "beautifulsoup4" },
|
|
201
200
|
{ name = "click" },
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
[package.optional-dependencies]
|
|
204
|
+
sync = [
|
|
205
|
+
{ name = "beautifulsoup4" },
|
|
202
206
|
{ name = "markdownify" },
|
|
203
207
|
{ name = "requests" },
|
|
204
208
|
]
|
|
@@ -206,20 +210,23 @@ dependencies = [
|
|
|
206
210
|
[package.dev-dependencies]
|
|
207
211
|
dev = [
|
|
208
212
|
{ name = "pytest" },
|
|
213
|
+
{ name = "readability-cli", extra = ["sync"] },
|
|
209
214
|
{ name = "ruff" },
|
|
210
215
|
]
|
|
211
216
|
|
|
212
217
|
[package.metadata]
|
|
213
218
|
requires-dist = [
|
|
214
|
-
{ name = "beautifulsoup4", specifier = ">=4.14.3" },
|
|
219
|
+
{ name = "beautifulsoup4", marker = "extra == 'sync'", specifier = ">=4.14.3" },
|
|
215
220
|
{ name = "click", specifier = ">=8.3.1" },
|
|
216
|
-
{ name = "markdownify", specifier = ">=1.2.2" },
|
|
217
|
-
{ name = "requests", specifier = ">=2.33.0" },
|
|
221
|
+
{ name = "markdownify", marker = "extra == 'sync'", specifier = ">=1.2.2" },
|
|
222
|
+
{ name = "requests", marker = "extra == 'sync'", specifier = ">=2.33.0" },
|
|
218
223
|
]
|
|
224
|
+
provides-extras = ["sync"]
|
|
219
225
|
|
|
220
226
|
[package.metadata.requires-dev]
|
|
221
227
|
dev = [
|
|
222
228
|
{ name = "pytest", specifier = ">=9.0.2" },
|
|
229
|
+
{ name = "readability-cli", extras = ["sync"] },
|
|
223
230
|
{ name = "ruff", specifier = ">=0.15.8" },
|
|
224
231
|
]
|
|
225
232
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|