readability-cli 0.9.0__tar.gz → 0.10.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {readability_cli-0.9.0 → readability_cli-0.10.1}/PKG-INFO +38 -5
- {readability_cli-0.9.0 → readability_cli-0.10.1}/README.md +37 -4
- {readability_cli-0.9.0 → readability_cli-0.10.1}/pyproject.toml +7 -1
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/checking.py +42 -29
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/cli.py +16 -30
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/configs/ruff.toml +8 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/guide.py +5 -7
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/outline.py +19 -43
- readability_cli-0.10.1/readability/tools.py +560 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/test_checking.py +674 -47
- {readability_cli-0.9.0 → readability_cli-0.10.1}/test_guide.py +9 -11
- {readability_cli-0.9.0 → readability_cli-0.10.1}/test_outline.py +16 -26
- {readability_cli-0.9.0 → readability_cli-0.10.1}/test_package.py +4 -2
- {readability_cli-0.9.0 → readability_cli-0.10.1}/uv.lock +1 -1
- readability_cli-0.9.0/readability/tools.py +0 -407
- {readability_cli-0.9.0 → readability_cli-0.10.1}/.github/workflows/ci.yml +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/.github/workflows/publish.yml +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/.github/workflows/update-guides.yml +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/.gitignore +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/.python-version +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/LICENSE +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/__init__.py +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/configs/biome-default.json +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/configs/pyrefly.toml +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/guides/Rguide.md +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/guides/cppguide.md +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/guides/csharp-style.md +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/guides/docguide-style.md +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/guides/go-guide.md +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/guides/htmlcssguide.md +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/guides/javaguide.md +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/guides/jsguide.md +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/guides/jsoncstyleguide.md +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/guides/objcguide.md +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/guides/pyguide.md +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/guides/shellguide.md +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/guides/tsguide.md +0 -0
- {readability_cli-0.9.0 → readability_cli-0.10.1}/readability/guides/vimscriptguide.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: readability-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.1
|
|
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
|
|
@@ -168,15 +168,21 @@ The returned `CheckReport` records only whether findings occurred and which
|
|
|
168
168
|
tools ran, were skipped, or failed. Detailed tool findings are still written as
|
|
169
169
|
each tool runs. Every path is validated before any tool runs, and a missing one
|
|
170
170
|
raises `FileNotFoundError` rather than being misreported as a finding. Relative
|
|
171
|
-
paths remain relative to the process working directory
|
|
172
|
-
configuration discovery
|
|
171
|
+
paths remain relative to the process working directory. `project_root` bounds
|
|
172
|
+
configuration discovery, locates project-local tool installs, and roots
|
|
173
|
+
ignore-file discovery. Because it bounds discovery, a caller that passes one
|
|
174
|
+
gets the same verdict for the same files wherever they sit, which is what makes
|
|
175
|
+
the result usable as a baseline. It defaults to the repository the process is
|
|
176
|
+
in, so the command agrees with the tools rather than with the directory the
|
|
177
|
+
caller happened to be standing in.
|
|
173
178
|
|
|
174
179
|
### Configuring Formats
|
|
175
180
|
|
|
176
181
|
Ruff and Pyrefly defaults follow the
|
|
177
182
|
[Google Python style guide](https://google.github.io/styleguide/pyguide.html):
|
|
178
|
-
80-column lines, Google docstrings, import
|
|
179
|
-
|
|
183
|
+
80-column lines, Google docstrings, one import per line sorted as isort's
|
|
184
|
+
`google` profile does, and full type checking. The Biome default applies the
|
|
185
|
+
80-column lines and two-space indentation of the
|
|
180
186
|
[Google JavaScript style guide](https://google.github.io/styleguide/jsguide.html)
|
|
181
187
|
and enables Biome's recommended lint rules. Project `biome.json` and
|
|
182
188
|
`biome.jsonc` files replace those bundled defaults for Biome-owned formats.
|
|
@@ -189,6 +195,33 @@ into it through that tool's native configuration. Gofmt has no project settings.
|
|
|
189
195
|
The bundled Biome file requires Biome 2.5 or later, matching the fallback
|
|
190
196
|
runner's version floor.
|
|
191
197
|
|
|
198
|
+
Configuration is found per file, in the file's own directory and then its
|
|
199
|
+
ancestors, which is where each canonical tool looks. A package keeping its
|
|
200
|
+
settings in a subdirectory is checked against them however it is reached:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# Uses web/biome.json, not the bundled default
|
|
204
|
+
readability check web/app.ts
|
|
205
|
+
|
|
206
|
+
# Uses the repository root's [tool.ruff], not the bundled default
|
|
207
|
+
cd pkg && readability check module.py
|
|
208
|
+
|
|
209
|
+
# pkg/ is checked against pkg/pyproject.toml, the rest against the defaults
|
|
210
|
+
readability check .
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
A tool is only told which configuration to use when the file has none, because
|
|
214
|
+
each one resolves its own hierarchy: a single Ruff invocation over two packages
|
|
215
|
+
declaring different line lengths reports each against its own. So a mixed tree
|
|
216
|
+
needs no choosing between configurations, and the bundled default never
|
|
217
|
+
overrides one the project declared.
|
|
218
|
+
|
|
219
|
+
Discovery is per tool, so configuring one leaves the others on their bundled
|
|
220
|
+
defaults. It is also bounded, by the repository the command runs in, or by
|
|
221
|
+
`project_root` when calling `check_paths` directly. Configuration outside that
|
|
222
|
+
boundary is not read, so the same paths get the same answer wherever the
|
|
223
|
+
repository sits.
|
|
224
|
+
|
|
192
225
|
## Style Guides
|
|
193
226
|
|
|
194
227
|
The `guide` command reads a Google style guide, using the local cache when
|
|
@@ -154,15 +154,21 @@ The returned `CheckReport` records only whether findings occurred and which
|
|
|
154
154
|
tools ran, were skipped, or failed. Detailed tool findings are still written as
|
|
155
155
|
each tool runs. Every path is validated before any tool runs, and a missing one
|
|
156
156
|
raises `FileNotFoundError` rather than being misreported as a finding. Relative
|
|
157
|
-
paths remain relative to the process working directory
|
|
158
|
-
configuration discovery
|
|
157
|
+
paths remain relative to the process working directory. `project_root` bounds
|
|
158
|
+
configuration discovery, locates project-local tool installs, and roots
|
|
159
|
+
ignore-file discovery. Because it bounds discovery, a caller that passes one
|
|
160
|
+
gets the same verdict for the same files wherever they sit, which is what makes
|
|
161
|
+
the result usable as a baseline. It defaults to the repository the process is
|
|
162
|
+
in, so the command agrees with the tools rather than with the directory the
|
|
163
|
+
caller happened to be standing in.
|
|
159
164
|
|
|
160
165
|
### Configuring Formats
|
|
161
166
|
|
|
162
167
|
Ruff and Pyrefly defaults follow the
|
|
163
168
|
[Google Python style guide](https://google.github.io/styleguide/pyguide.html):
|
|
164
|
-
80-column lines, Google docstrings, import
|
|
165
|
-
|
|
169
|
+
80-column lines, Google docstrings, one import per line sorted as isort's
|
|
170
|
+
`google` profile does, and full type checking. The Biome default applies the
|
|
171
|
+
80-column lines and two-space indentation of the
|
|
166
172
|
[Google JavaScript style guide](https://google.github.io/styleguide/jsguide.html)
|
|
167
173
|
and enables Biome's recommended lint rules. Project `biome.json` and
|
|
168
174
|
`biome.jsonc` files replace those bundled defaults for Biome-owned formats.
|
|
@@ -175,6 +181,33 @@ into it through that tool's native configuration. Gofmt has no project settings.
|
|
|
175
181
|
The bundled Biome file requires Biome 2.5 or later, matching the fallback
|
|
176
182
|
runner's version floor.
|
|
177
183
|
|
|
184
|
+
Configuration is found per file, in the file's own directory and then its
|
|
185
|
+
ancestors, which is where each canonical tool looks. A package keeping its
|
|
186
|
+
settings in a subdirectory is checked against them however it is reached:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Uses web/biome.json, not the bundled default
|
|
190
|
+
readability check web/app.ts
|
|
191
|
+
|
|
192
|
+
# Uses the repository root's [tool.ruff], not the bundled default
|
|
193
|
+
cd pkg && readability check module.py
|
|
194
|
+
|
|
195
|
+
# pkg/ is checked against pkg/pyproject.toml, the rest against the defaults
|
|
196
|
+
readability check .
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
A tool is only told which configuration to use when the file has none, because
|
|
200
|
+
each one resolves its own hierarchy: a single Ruff invocation over two packages
|
|
201
|
+
declaring different line lengths reports each against its own. So a mixed tree
|
|
202
|
+
needs no choosing between configurations, and the bundled default never
|
|
203
|
+
overrides one the project declared.
|
|
204
|
+
|
|
205
|
+
Discovery is per tool, so configuring one leaves the others on their bundled
|
|
206
|
+
defaults. It is also bounded, by the repository the command runs in, or by
|
|
207
|
+
`project_root` when calling `check_paths` directly. Configuration outside that
|
|
208
|
+
boundary is not read, so the same paths get the same answer wherever the
|
|
209
|
+
repository sits.
|
|
210
|
+
|
|
178
211
|
## Style Guides
|
|
179
212
|
|
|
180
213
|
The `guide` command reads a Google style guide, using the local cache when
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "readability-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.10.1"
|
|
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"
|
|
@@ -47,6 +47,12 @@ ignore = ["PLR0911", "PLR0912", "PLR0913", "PLR0915", "PLR2004"]
|
|
|
47
47
|
[tool.ruff.lint.pydocstyle]
|
|
48
48
|
convention = "google"
|
|
49
49
|
|
|
50
|
+
[tool.ruff.lint.isort]
|
|
51
|
+
force-single-line = true
|
|
52
|
+
force-sort-within-sections = true
|
|
53
|
+
single-line-exclusions = ["typing"]
|
|
54
|
+
order-by-type = false
|
|
55
|
+
|
|
50
56
|
[tool.ruff.lint.per-file-ignores]
|
|
51
57
|
"test_*.py" = ["D"]
|
|
52
58
|
"*_test.py" = ["D"]
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
"""Run readability checks and report their coverage."""
|
|
2
2
|
|
|
3
|
+
from collections.abc import Sequence
|
|
3
4
|
import dataclasses
|
|
4
5
|
import logging
|
|
5
|
-
import subprocess
|
|
6
|
-
from collections.abc import Sequence
|
|
7
6
|
from pathlib import Path
|
|
7
|
+
import subprocess
|
|
8
8
|
|
|
9
9
|
import click
|
|
10
10
|
|
|
11
|
-
from readability.tools import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
)
|
|
11
|
+
from readability.tools import _command_batches
|
|
12
|
+
from readability.tools import _get_tool_definitions
|
|
13
|
+
from readability.tools import _repository_root
|
|
14
|
+
from readability.tools import _should_run_tool
|
|
15
|
+
from readability.tools import _tool_is_installed
|
|
16
|
+
from readability.tools import ToolPlan
|
|
18
17
|
|
|
19
18
|
logger = logging.getLogger("readability")
|
|
20
19
|
DEFAULT_TIMEOUT = 60
|
|
@@ -74,8 +73,10 @@ def check_paths(
|
|
|
74
73
|
Args:
|
|
75
74
|
paths: Files or directories to check, as strings or paths. Relative
|
|
76
75
|
paths are interpreted from the current working directory.
|
|
77
|
-
project_root:
|
|
78
|
-
|
|
76
|
+
project_root: Bounds config discovery, so the same files get the
|
|
77
|
+
same verdict wherever the tree sits, and locates tool installs
|
|
78
|
+
and ignore files. Defaults to the repository the process is in.
|
|
79
|
+
It does not rebase paths.
|
|
79
80
|
fix: Whether to apply automatic fixes.
|
|
80
81
|
|
|
81
82
|
Returns:
|
|
@@ -85,7 +86,11 @@ def check_paths(
|
|
|
85
86
|
FileNotFoundError: If any requested path does not exist. Every path is
|
|
86
87
|
validated before any tools run.
|
|
87
88
|
"""
|
|
88
|
-
|
|
89
|
+
here = Path.cwd()
|
|
90
|
+
repository = _repository_root(here)
|
|
91
|
+
root = project_root if project_root is not None else (repository or here)
|
|
92
|
+
# A named root bounds the search; otherwise each path's repository does
|
|
93
|
+
boundary = project_root
|
|
89
94
|
requested_paths = [Path(path) for path in paths]
|
|
90
95
|
missing_path = next(
|
|
91
96
|
(path for path in requested_paths if not path.exists()), None
|
|
@@ -95,7 +100,7 @@ def check_paths(
|
|
|
95
100
|
|
|
96
101
|
report = CheckReport()
|
|
97
102
|
for path in requested_paths:
|
|
98
|
-
path_report = _check_path(path, root, fix=fix)
|
|
103
|
+
path_report = _check_path(path, root, boundary, fix=fix)
|
|
99
104
|
if not path_report.ran:
|
|
100
105
|
path_report.unverified_paths.append(path)
|
|
101
106
|
report.absorb(path_report)
|
|
@@ -103,13 +108,18 @@ def check_paths(
|
|
|
103
108
|
|
|
104
109
|
|
|
105
110
|
def _check_path(
|
|
106
|
-
path: Path,
|
|
111
|
+
path: Path,
|
|
112
|
+
project_root: Path,
|
|
113
|
+
boundary: Path | None = None,
|
|
114
|
+
fix: bool = False,
|
|
107
115
|
) -> CheckReport:
|
|
108
116
|
"""Apply relevant tools to a single path.
|
|
109
117
|
|
|
110
118
|
Args:
|
|
111
119
|
path: The path (file or directory) to check.
|
|
112
|
-
project_root:
|
|
120
|
+
project_root: Locates tool installs and ignore files.
|
|
121
|
+
boundary: Outermost directory config discovery may consider, or
|
|
122
|
+
None to bound it by the path's own repository.
|
|
113
123
|
fix: Whether to apply automatic fixes.
|
|
114
124
|
|
|
115
125
|
Returns:
|
|
@@ -117,14 +127,16 @@ def _check_path(
|
|
|
117
127
|
"""
|
|
118
128
|
logger.info("Checking path: %s", path)
|
|
119
129
|
|
|
130
|
+
if boundary is None:
|
|
131
|
+
boundary = _repository_root(path) or Path(path.resolve().anchor)
|
|
132
|
+
|
|
120
133
|
# Iterate through all supported tool definitions
|
|
121
134
|
report = CheckReport()
|
|
122
|
-
for tool in _get_tool_definitions(path, project_root):
|
|
135
|
+
for tool in _get_tool_definitions(path, project_root, boundary):
|
|
123
136
|
if not _should_run_tool(tool, path):
|
|
124
137
|
continue
|
|
125
138
|
|
|
126
|
-
# A
|
|
127
|
-
# which is not the same as a clean result
|
|
139
|
+
# A wanted but absent tool is a hole in coverage, not a clean result
|
|
128
140
|
if not _tool_is_installed(tool):
|
|
129
141
|
report.skipped.add(tool.name)
|
|
130
142
|
continue
|
|
@@ -156,10 +168,7 @@ def _run_tool(
|
|
|
156
168
|
target_count = len(tool.targets or ())
|
|
157
169
|
try:
|
|
158
170
|
if fix:
|
|
159
|
-
#
|
|
160
|
-
# exit non-zero when something is left over, which is a finding
|
|
161
|
-
# rather than a failure, so the check below still gets to run
|
|
162
|
-
# and report what they could not deal with.
|
|
171
|
+
# Leftovers exit non-zero, which is a finding rather than a failure
|
|
163
172
|
for phase in ("format", "fix"):
|
|
164
173
|
configured_command = getattr(tool, phase)
|
|
165
174
|
if configured_command:
|
|
@@ -208,8 +217,7 @@ def _run_tool(
|
|
|
208
217
|
f"{result.stdout}\n{result.stderr}"
|
|
209
218
|
)
|
|
210
219
|
|
|
211
|
-
#
|
|
212
|
-
# nothing. Whatever it managed before that stays in ran.
|
|
220
|
+
# Never starting, or timing out, means this tool checked nothing
|
|
213
221
|
except (subprocess.SubprocessError, OSError) as e:
|
|
214
222
|
logger.warning("Could not run %s: %s", tool.name, e)
|
|
215
223
|
report.failed.add(tool.name)
|
|
@@ -227,14 +235,19 @@ def _tool_checked_files(
|
|
|
227
235
|
result: The completed subprocess.
|
|
228
236
|
|
|
229
237
|
Returns:
|
|
230
|
-
False when
|
|
238
|
+
False when the tool explicitly reports that it processed no files.
|
|
231
239
|
"""
|
|
232
|
-
|
|
240
|
+
# Biome says this for an unmatched target, Ruff when excludes cover them all
|
|
241
|
+
zero_file_reports = {
|
|
242
|
+
"biome": ("Checked 0 files", "Formatted 0 files"),
|
|
243
|
+
"ruff": ("No Python files found",),
|
|
244
|
+
}
|
|
245
|
+
summaries = zero_file_reports.get(tool_name)
|
|
246
|
+
if summaries is None:
|
|
233
247
|
return True
|
|
248
|
+
|
|
234
249
|
output = f"{result.stdout or ''}\n{result.stderr or ''}"
|
|
235
|
-
|
|
236
|
-
zero_file_summaries = ("Checked 0 files", "Formatted 0 files")
|
|
237
|
-
return not any(summary in output for summary in zero_file_summaries)
|
|
250
|
+
return not any(summary in output for summary in summaries)
|
|
238
251
|
|
|
239
252
|
|
|
240
253
|
def _capture_tool_command(
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
"""Command-line interface for readability."""
|
|
2
2
|
|
|
3
|
+
from collections.abc import Sequence
|
|
3
4
|
import logging
|
|
4
5
|
import os
|
|
5
6
|
import sys
|
|
6
|
-
from collections.abc import Sequence
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
9
|
|
|
10
10
|
from readability.checking import check_paths
|
|
11
|
-
from readability.guide import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
from readability.outline import _echo_outline, _select_section
|
|
11
|
+
from readability.guide import get_guide
|
|
12
|
+
from readability.guide import get_guides_dir
|
|
13
|
+
from readability.guide import get_local_path
|
|
14
|
+
from readability.guide import LANGUAGE_MAP
|
|
15
|
+
from readability.guide import refresh_guide
|
|
16
|
+
from readability.outline import _echo_outline
|
|
17
|
+
from readability.outline import _select_section
|
|
19
18
|
|
|
20
19
|
logger = logging.getLogger("readability")
|
|
21
20
|
|
|
@@ -60,8 +59,7 @@ def guide(
|
|
|
60
59
|
_echo_languages()
|
|
61
60
|
return
|
|
62
61
|
|
|
63
|
-
# Refusing beats
|
|
64
|
-
# caller ends up reading the wrong thing without being told.
|
|
62
|
+
# Refusing beats a silent precedence rule the caller cannot see
|
|
65
63
|
if full and reference:
|
|
66
64
|
raise click.UsageError("--full takes the whole guide, so REF cannot.")
|
|
67
65
|
|
|
@@ -106,8 +104,7 @@ def _resolve_filenames(languages: Sequence[str]) -> list[str]:
|
|
|
106
104
|
f"Language '{language}' is not supported. Supported "
|
|
107
105
|
f"languages: {', '.join(sorted(LANGUAGE_MAP.keys()))}"
|
|
108
106
|
)
|
|
109
|
-
# Aliases
|
|
110
|
-
# fetched once however many of its names were given
|
|
107
|
+
# Aliases share a guide, fetched once however many names were given
|
|
111
108
|
if filename not in filenames:
|
|
112
109
|
filenames.append(filename)
|
|
113
110
|
|
|
@@ -190,8 +187,7 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
|
|
|
190
187
|
err=True,
|
|
191
188
|
)
|
|
192
189
|
|
|
193
|
-
# A tool that
|
|
194
|
-
# so its silence is not a pass however far the rest of the run got
|
|
190
|
+
# A tool that never ran checked nothing, so its silence is not a pass
|
|
195
191
|
if report.failed:
|
|
196
192
|
click.echo(
|
|
197
193
|
f"Error: Could not run: {', '.join(sorted(report.failed))}. "
|
|
@@ -200,14 +196,11 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
|
|
|
200
196
|
)
|
|
201
197
|
sys.exit(1)
|
|
202
198
|
|
|
203
|
-
# Findings
|
|
204
|
-
# not process any files (for example, a Biome configuration error).
|
|
199
|
+
# Findings fail the check even if the tool processed no files
|
|
205
200
|
if report.findings:
|
|
206
201
|
sys.exit(1)
|
|
207
202
|
|
|
208
|
-
# Having run nothing is not a pass
|
|
209
|
-
# command became a silent no-op wherever its tools were absent, gating
|
|
210
|
-
# nothing while every caller read the exit code as approval.
|
|
203
|
+
# Having run nothing is not a pass, it is a silent no-op gating nothing
|
|
211
204
|
if not report.ran and report.skipped:
|
|
212
205
|
click.echo(
|
|
213
206
|
f"Error: Every tool for {len(paths)} path(s) is missing, so "
|
|
@@ -216,9 +209,7 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
|
|
|
216
209
|
)
|
|
217
210
|
sys.exit(1)
|
|
218
211
|
|
|
219
|
-
# No tool applying is a fact
|
|
220
|
-
# is nothing to install and nothing to fix. It still cannot be reported
|
|
221
|
-
# as a clean result, because nothing was inspected.
|
|
212
|
+
# No tool applying is a fact, not a fault, but still not a clean result
|
|
222
213
|
if not report.ran:
|
|
223
214
|
click.echo(
|
|
224
215
|
f"No tool applies to {len(paths)} path(s); nothing was checked.",
|
|
@@ -235,8 +226,7 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
|
|
|
235
226
|
err=True,
|
|
236
227
|
)
|
|
237
228
|
|
|
238
|
-
#
|
|
239
|
-
# nothing at all and left the caller unable to tell it from a no-op.
|
|
229
|
+
# Without this a clean run printed nothing and read as a no-op
|
|
240
230
|
click.echo(
|
|
241
231
|
f"No findings in {checked_path_count} path(s) "
|
|
242
232
|
f"({', '.join(sorted(report.ran))}).",
|
|
@@ -247,11 +237,7 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
|
|
|
247
237
|
# Main entry point for the CLI
|
|
248
238
|
def main() -> None:
|
|
249
239
|
"""Main entry point for the CLI."""
|
|
250
|
-
#
|
|
251
|
-
# module as a library (e.g. from lemming) has no side effects
|
|
252
|
-
# WARNING, not INFO: everything logged below it narrates progress, which
|
|
253
|
-
# is what --verbose is for. Leaving it on meant there was no quiet mode
|
|
254
|
-
# and the flag could only add DEBUG on top.
|
|
240
|
+
# Configured here, not at import, so library use has no side effects
|
|
255
241
|
logging.basicConfig(
|
|
256
242
|
level=logging.WARNING,
|
|
257
243
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
@@ -14,6 +14,14 @@ ignore = ["PLR0911", "PLR0912", "PLR0913", "PLR0915", "PLR2004"]
|
|
|
14
14
|
[lint.pydocstyle]
|
|
15
15
|
convention = "google"
|
|
16
16
|
|
|
17
|
+
[lint.isort]
|
|
18
|
+
# Mirrors isort's google profile; its lexicographical and group-by-package
|
|
19
|
+
# options have no ruff equivalent
|
|
20
|
+
force-single-line = true
|
|
21
|
+
force-sort-within-sections = true
|
|
22
|
+
single-line-exclusions = ["typing"]
|
|
23
|
+
order-by-type = false
|
|
24
|
+
|
|
17
25
|
[lint.per-file-ignores]
|
|
18
26
|
# Docstring requirements are noise in test files
|
|
19
27
|
"test_*.py" = ["D"]
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"""Fetch and cache Google style guides."""
|
|
2
2
|
|
|
3
|
+
from importlib.resources import files
|
|
3
4
|
import logging
|
|
4
5
|
import os
|
|
5
6
|
import warnings
|
|
6
|
-
from importlib.resources import files
|
|
7
7
|
|
|
8
|
+
from bs4 import BeautifulSoup
|
|
9
|
+
from bs4 import XMLParsedAsHTMLWarning
|
|
8
10
|
import click
|
|
9
|
-
import requests
|
|
10
|
-
from bs4 import BeautifulSoup, XMLParsedAsHTMLWarning
|
|
11
11
|
from markdownify import markdownify as md
|
|
12
|
+
import requests
|
|
12
13
|
|
|
13
14
|
warnings.filterwarnings("ignore", category=XMLParsedAsHTMLWarning)
|
|
14
15
|
|
|
@@ -55,10 +56,7 @@ LANGUAGE_MAP = {
|
|
|
55
56
|
|
|
56
57
|
BASE_URL = "https://google.github.io/styleguide/"
|
|
57
58
|
|
|
58
|
-
#
|
|
59
|
-
# their underscores leaves 'from \_\_future\_\_ import' in the text a caller
|
|
60
|
-
# greps or reads. CommonMark would render a bare '__future__' as emphasis, but
|
|
61
|
-
# these guides are consumed as plain text, where fidelity matters more.
|
|
59
|
+
# Escaping underscores would leave 'from \_\_future\_\_ import' in the text
|
|
62
60
|
MARKDOWNIFY_OPTIONS = {"heading_style": "ATX", "escape_underscores": False}
|
|
63
61
|
|
|
64
62
|
|
|
@@ -1,31 +1,27 @@
|
|
|
1
1
|
"""Parse, search, and render style-guide outlines."""
|
|
2
2
|
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from dataclasses import dataclass
|
|
3
6
|
import re
|
|
4
7
|
import sys
|
|
5
|
-
from collections.abc import Iterator, Sequence
|
|
6
|
-
from dataclasses import dataclass
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
9
10
|
import click
|
|
10
11
|
|
|
11
|
-
#
|
|
12
|
-
# document structure. Guides for `#`-commented languages (shell, Python) would
|
|
13
|
-
# otherwise report hundreds of code comments as headings.
|
|
12
|
+
# Inside a fence is sample code, where a '#' comment is not a heading
|
|
14
13
|
FENCE_PATTERN = re.compile(r"^\s{0,3}(?:```|~~~)")
|
|
15
14
|
|
|
16
|
-
# Closing hashes are
|
|
17
|
-
# a title such as 'C#' keeps its trailing character.
|
|
15
|
+
# Closing hashes are space-separated, so a title like 'C#' keeps its own
|
|
18
16
|
HEADING_PATTERN = re.compile(r"^(#{1,6})\s+(.*?)(?:\s+#+)?\s*$")
|
|
19
17
|
|
|
20
|
-
# A
|
|
18
|
+
# A number the document prints itself, e.g. '2.2' in '2.2 Imports'
|
|
21
19
|
DOCUMENT_NUMBER_PATTERN = re.compile(r"^(\d+(?:\.\d+)*)\.?\s+(.+)$")
|
|
22
20
|
|
|
23
|
-
#
|
|
24
|
-
# otherwise trail the end of the preceding section.
|
|
21
|
+
# Link targets sit above their heading, not at the end of the one before
|
|
25
22
|
ANCHOR_PATTERN = re.compile(r'^<a id="[^"]*"></a>$')
|
|
26
23
|
|
|
27
|
-
#
|
|
28
|
-
# both sides are required so that a heading like '`Array<T>` Type' stays whole.
|
|
24
|
+
# Scoped reference separator; spaces required so '`Array<T>` Type' stays whole
|
|
29
25
|
PATH_SEPARATOR_PATTERN = re.compile(r"\s+>\s+")
|
|
30
26
|
|
|
31
27
|
|
|
@@ -148,8 +144,7 @@ def parse_headings(content: str) -> list[Heading]:
|
|
|
148
144
|
if not raw:
|
|
149
145
|
return []
|
|
150
146
|
|
|
151
|
-
#
|
|
152
|
-
# ('2.2') is the reliable signal; a bare leading digit is not.
|
|
147
|
+
# A dotted number is the reliable signal; a bare leading digit is not
|
|
153
148
|
document_numbers = [
|
|
154
149
|
match.group(1)
|
|
155
150
|
for _, _, text in raw
|
|
@@ -157,8 +152,7 @@ def parse_headings(content: str) -> list[Heading]:
|
|
|
157
152
|
]
|
|
158
153
|
numbered = any("." in number for number in document_numbers)
|
|
159
154
|
|
|
160
|
-
# A lone top-level heading is the
|
|
161
|
-
# than being the first section, so numbering starts with its children.
|
|
155
|
+
# A lone top-level heading is the title, so numbering starts below it
|
|
162
156
|
top_level = min(level for _, level, _ in raw)
|
|
163
157
|
top_level_lines = [line for line, level, _ in raw if level == top_level]
|
|
164
158
|
title_line = top_level_lines[0] if len(top_level_lines) == 1 else None
|
|
@@ -169,12 +163,7 @@ def parse_headings(content: str) -> list[Heading]:
|
|
|
169
163
|
index = "" if line == title_line else _next_index(open_headings, level)
|
|
170
164
|
number, title = _split_document_number(text, numbered)
|
|
171
165
|
|
|
172
|
-
# A guide
|
|
173
|
-
# are called, so its numbers address them. A positional index would
|
|
174
|
-
# drift wherever the guide skips one — pyguide has no 2.15 at all,
|
|
175
|
-
# and calling its 2.16 by that name would cite a section that does
|
|
176
|
-
# not exist. The tree still advances above, so headings the guide
|
|
177
|
-
# leaves unnumbered keep a positional index to be reachable by.
|
|
166
|
+
# A guide's own numbers address its sections; an index would drift
|
|
178
167
|
if number:
|
|
179
168
|
index = number
|
|
180
169
|
headings.append(
|
|
@@ -203,10 +192,7 @@ def _slugify(text: str) -> str:
|
|
|
203
192
|
return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
|
|
204
193
|
|
|
205
194
|
|
|
206
|
-
#
|
|
207
|
-
# over the shipped corpus, it marks the 4% that are expensive while leaving
|
|
208
|
-
# the rest unannotated: a size on every line would read '0' on 59% of them,
|
|
209
|
-
# which is noise in the one output whose job is to be scanned quickly.
|
|
195
|
+
# Marks the expensive 4% of the corpus; a size on every line would be noise
|
|
210
196
|
LARGE_SECTION_WORDS = 1200
|
|
211
197
|
|
|
212
198
|
|
|
@@ -263,8 +249,7 @@ def format_outline(headings: Sequence[Heading], content: str = "") -> str:
|
|
|
263
249
|
section = extract_section(content, headings, position)
|
|
264
250
|
size = _format_size(len(section.split()))
|
|
265
251
|
|
|
266
|
-
# The index already carries the guide's
|
|
267
|
-
# so printing the heading verbatim would show it twice.
|
|
252
|
+
# The index already carries the guide's number where it has one
|
|
268
253
|
lines.append(f"{indent}{prefix}{heading.title}{size}")
|
|
269
254
|
|
|
270
255
|
return "\n".join(lines)
|
|
@@ -281,8 +266,7 @@ def _matches_component(heading: Heading, component: str, exact: bool) -> bool:
|
|
|
281
266
|
Returns:
|
|
282
267
|
True if the component identifies the heading.
|
|
283
268
|
"""
|
|
284
|
-
# Numbers
|
|
285
|
-
# match unrelated sections.
|
|
269
|
+
# Numbers compare in full; a substring would match unrelated sections
|
|
286
270
|
wanted = component.strip().rstrip(".")
|
|
287
271
|
if wanted and wanted in {heading.index, heading.number}:
|
|
288
272
|
return True
|
|
@@ -369,8 +353,7 @@ def find_headings(headings: Sequence[Heading], reference: str) -> list[int]:
|
|
|
369
353
|
if not components:
|
|
370
354
|
return []
|
|
371
355
|
|
|
372
|
-
#
|
|
373
|
-
# in full, so 'Imports' does not also select 'Imports and Exports'.
|
|
356
|
+
# Whole matches first, so 'Imports' does not select 'Imports and Exports'
|
|
374
357
|
for exact in (True, False):
|
|
375
358
|
matches = [
|
|
376
359
|
position
|
|
@@ -512,9 +495,7 @@ def find_mentions(
|
|
|
512
495
|
|
|
513
496
|
lines = content.splitlines()
|
|
514
497
|
|
|
515
|
-
#
|
|
516
|
-
# because it is the whole guide rather than a section within it, so
|
|
517
|
-
# crediting the preamble to it would suggest reading everything.
|
|
498
|
+
# The title has no index, so crediting it would suggest reading everything
|
|
518
499
|
sections = [
|
|
519
500
|
(position, _section_bounds(lines, headings, position))
|
|
520
501
|
for position in range(len(headings))
|
|
@@ -526,10 +507,7 @@ def find_mentions(
|
|
|
526
507
|
if needle not in line.lower():
|
|
527
508
|
continue
|
|
528
509
|
|
|
529
|
-
# Bounds nest, so the last
|
|
530
|
-
# innermost one that prints it. A section sheds the anchors of the
|
|
531
|
-
# heading below it, and those fall to an ancestor rather than to the
|
|
532
|
-
# section they merely trail.
|
|
510
|
+
# Bounds nest, so the last one covering a line is what prints it
|
|
533
511
|
holders = [
|
|
534
512
|
position
|
|
535
513
|
for position, (start, end) in sections
|
|
@@ -577,8 +555,7 @@ def _echo_outline(content: str, language: str) -> None:
|
|
|
577
555
|
example = _example_reference(headings)
|
|
578
556
|
if example:
|
|
579
557
|
count = sum(1 for heading in headings if heading.index)
|
|
580
|
-
# stdout is block-buffered
|
|
581
|
-
# lands above the outline it is meant to follow
|
|
558
|
+
# Redirected stdout is block-buffered, so the hint would land first
|
|
582
559
|
sys.stdout.flush()
|
|
583
560
|
click.echo(
|
|
584
561
|
f"# {count} sections · print one: "
|
|
@@ -656,8 +633,7 @@ def _select_section(content: str, reference: str, language: str) -> str:
|
|
|
656
633
|
if not matches:
|
|
657
634
|
_report_no_heading(content, headings, reference, language)
|
|
658
635
|
|
|
659
|
-
#
|
|
660
|
-
# guides repeat headings ('Decision' appears under every Python rule).
|
|
636
|
+
# Guides repeat headings, so name every candidate rather than guess
|
|
661
637
|
if len(matches) > 1:
|
|
662
638
|
click.echo(
|
|
663
639
|
f"Error: '{reference}' matches {len(matches)} headings in the "
|