readability-cli 0.10.0__tar.gz → 0.10.2__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.0 → readability_cli-0.10.2}/PKG-INFO +12 -3
- {readability_cli-0.10.0 → readability_cli-0.10.2}/README.md +11 -2
- {readability_cli-0.10.0 → readability_cli-0.10.2}/pyproject.toml +7 -1
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/checking.py +21 -14
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/cli.py +16 -11
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/configs/ruff.toml +8 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/guide.py +5 -7
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/outline.py +19 -43
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/tools.py +16 -5
- {readability_cli-0.10.0 → readability_cli-0.10.2}/test_checking.py +166 -14
- {readability_cli-0.10.0 → readability_cli-0.10.2}/test_guide.py +9 -11
- {readability_cli-0.10.0 → readability_cli-0.10.2}/test_outline.py +16 -26
- {readability_cli-0.10.0 → readability_cli-0.10.2}/test_package.py +4 -2
- {readability_cli-0.10.0 → readability_cli-0.10.2}/uv.lock +1 -1
- {readability_cli-0.10.0 → readability_cli-0.10.2}/.github/workflows/ci.yml +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/.github/workflows/publish.yml +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/.github/workflows/update-guides.yml +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/.gitignore +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/.python-version +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/LICENSE +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/__init__.py +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/configs/biome-default.json +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/configs/pyrefly.toml +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/guides/Rguide.md +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/guides/cppguide.md +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/guides/csharp-style.md +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/guides/docguide-style.md +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/guides/go-guide.md +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/guides/htmlcssguide.md +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/guides/javaguide.md +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/guides/jsguide.md +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/guides/jsoncstyleguide.md +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/guides/objcguide.md +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/guides/pyguide.md +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/guides/shellguide.md +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/guides/tsguide.md +0 -0
- {readability_cli-0.10.0 → readability_cli-0.10.2}/readability/guides/vimscriptguide.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: readability-cli
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.2
|
|
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
|
|
@@ -90,8 +90,16 @@ readability check src/ tests/ main.py
|
|
|
90
90
|
|
|
91
91
|
# Automatically fix and format files
|
|
92
92
|
readability check . --fix
|
|
93
|
+
|
|
94
|
+
# Also apply fixes that may change behavior (implies --fix)
|
|
95
|
+
readability check . --unsafe
|
|
93
96
|
```
|
|
94
97
|
|
|
98
|
+
Fixes that may change a program's behavior or drop its comments are opt-in
|
|
99
|
+
through `--unsafe`, which every tool applies under its own name: Ruff's
|
|
100
|
+
`--unsafe-fixes` and Biome's `--unsafe`. Tools drawing no such distinction,
|
|
101
|
+
such as gofmt, are unaffected by it.
|
|
102
|
+
|
|
95
103
|
### Supported Formats
|
|
96
104
|
|
|
97
105
|
| Formats | Owners | Project configuration |
|
|
@@ -180,8 +188,9 @@ caller happened to be standing in.
|
|
|
180
188
|
|
|
181
189
|
Ruff and Pyrefly defaults follow the
|
|
182
190
|
[Google Python style guide](https://google.github.io/styleguide/pyguide.html):
|
|
183
|
-
80-column lines, Google docstrings, import
|
|
184
|
-
|
|
191
|
+
80-column lines, Google docstrings, one import per line sorted as isort's
|
|
192
|
+
`google` profile does, and full type checking. The Biome default applies the
|
|
193
|
+
80-column lines and two-space indentation of the
|
|
185
194
|
[Google JavaScript style guide](https://google.github.io/styleguide/jsguide.html)
|
|
186
195
|
and enables Biome's recommended lint rules. Project `biome.json` and
|
|
187
196
|
`biome.jsonc` files replace those bundled defaults for Biome-owned formats.
|
|
@@ -76,8 +76,16 @@ readability check src/ tests/ main.py
|
|
|
76
76
|
|
|
77
77
|
# Automatically fix and format files
|
|
78
78
|
readability check . --fix
|
|
79
|
+
|
|
80
|
+
# Also apply fixes that may change behavior (implies --fix)
|
|
81
|
+
readability check . --unsafe
|
|
79
82
|
```
|
|
80
83
|
|
|
84
|
+
Fixes that may change a program's behavior or drop its comments are opt-in
|
|
85
|
+
through `--unsafe`, which every tool applies under its own name: Ruff's
|
|
86
|
+
`--unsafe-fixes` and Biome's `--unsafe`. Tools drawing no such distinction,
|
|
87
|
+
such as gofmt, are unaffected by it.
|
|
88
|
+
|
|
81
89
|
### Supported Formats
|
|
82
90
|
|
|
83
91
|
| Formats | Owners | Project configuration |
|
|
@@ -166,8 +174,9 @@ caller happened to be standing in.
|
|
|
166
174
|
|
|
167
175
|
Ruff and Pyrefly defaults follow the
|
|
168
176
|
[Google Python style guide](https://google.github.io/styleguide/pyguide.html):
|
|
169
|
-
80-column lines, Google docstrings, import
|
|
170
|
-
|
|
177
|
+
80-column lines, Google docstrings, one import per line sorted as isort's
|
|
178
|
+
`google` profile does, and full type checking. The Biome default applies the
|
|
179
|
+
80-column lines and two-space indentation of the
|
|
171
180
|
[Google JavaScript style guide](https://google.github.io/styleguide/jsguide.html)
|
|
172
181
|
and enables Biome's recommended lint rules. Project `biome.json` and
|
|
173
182
|
`biome.jsonc` files replace those bundled defaults for Biome-owned formats.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "readability-cli"
|
|
3
|
-
version = "0.10.
|
|
3
|
+
version = "0.10.2"
|
|
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,21 +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
|
-
_tool_is_installed,
|
|
18
|
-
)
|
|
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
|
|
19
17
|
|
|
20
18
|
logger = logging.getLogger("readability")
|
|
21
19
|
DEFAULT_TIMEOUT = 60
|
|
@@ -64,6 +62,7 @@ def check_paths(
|
|
|
64
62
|
paths: Sequence[str | Path],
|
|
65
63
|
project_root: Path | None = None,
|
|
66
64
|
fix: bool = False,
|
|
65
|
+
unsafe: bool = False,
|
|
67
66
|
) -> CheckReport:
|
|
68
67
|
"""Run relevant checks for paths and aggregate what the tools did.
|
|
69
68
|
|
|
@@ -80,6 +79,8 @@ def check_paths(
|
|
|
80
79
|
and ignore files. Defaults to the repository the process is in.
|
|
81
80
|
It does not rebase paths.
|
|
82
81
|
fix: Whether to apply automatic fixes.
|
|
82
|
+
unsafe: Whether those fixes may change behavior or drop comments.
|
|
83
|
+
Tools that draw no such distinction are unaffected.
|
|
83
84
|
|
|
84
85
|
Returns:
|
|
85
86
|
A report aggregated across all provided paths.
|
|
@@ -102,7 +103,7 @@ def check_paths(
|
|
|
102
103
|
|
|
103
104
|
report = CheckReport()
|
|
104
105
|
for path in requested_paths:
|
|
105
|
-
path_report = _check_path(path, root, boundary, fix=fix)
|
|
106
|
+
path_report = _check_path(path, root, boundary, fix=fix, unsafe=unsafe)
|
|
106
107
|
if not path_report.ran:
|
|
107
108
|
path_report.unverified_paths.append(path)
|
|
108
109
|
report.absorb(path_report)
|
|
@@ -114,6 +115,7 @@ def _check_path(
|
|
|
114
115
|
project_root: Path,
|
|
115
116
|
boundary: Path | None = None,
|
|
116
117
|
fix: bool = False,
|
|
118
|
+
unsafe: bool = False,
|
|
117
119
|
) -> CheckReport:
|
|
118
120
|
"""Apply relevant tools to a single path.
|
|
119
121
|
|
|
@@ -123,6 +125,7 @@ def _check_path(
|
|
|
123
125
|
boundary: Outermost directory config discovery may consider, or
|
|
124
126
|
None to bound it by the path's own repository.
|
|
125
127
|
fix: Whether to apply automatic fixes.
|
|
128
|
+
unsafe: Whether those fixes may change behavior or drop comments.
|
|
126
129
|
|
|
127
130
|
Returns:
|
|
128
131
|
What the tools applicable to this path did.
|
|
@@ -143,7 +146,7 @@ def _check_path(
|
|
|
143
146
|
report.skipped.add(tool.name)
|
|
144
147
|
continue
|
|
145
148
|
|
|
146
|
-
report.absorb(_run_tool(tool, fix=fix))
|
|
149
|
+
report.absorb(_run_tool(tool, fix=fix, unsafe=unsafe))
|
|
147
150
|
|
|
148
151
|
return report
|
|
149
152
|
|
|
@@ -151,12 +154,14 @@ def _check_path(
|
|
|
151
154
|
def _run_tool(
|
|
152
155
|
tool: ToolPlan,
|
|
153
156
|
fix: bool = False,
|
|
157
|
+
unsafe: bool = False,
|
|
154
158
|
) -> CheckReport:
|
|
155
159
|
"""Orchestrate the execution of a specific formatting or linting tool.
|
|
156
160
|
|
|
157
161
|
Args:
|
|
158
162
|
tool: The executable plan to run.
|
|
159
163
|
fix: Whether to apply automatic fixes.
|
|
164
|
+
unsafe: Whether those fixes may change behavior or drop comments.
|
|
160
165
|
|
|
161
166
|
Returns:
|
|
162
167
|
What the tool did: whether it reported findings, and whether it
|
|
@@ -170,9 +175,11 @@ def _run_tool(
|
|
|
170
175
|
target_count = len(tool.targets or ())
|
|
171
176
|
try:
|
|
172
177
|
if fix:
|
|
178
|
+
# A tool with no unsafe mode still fixes what it safely can
|
|
179
|
+
unsafe_fix = tool.unsafe_fix if unsafe else ()
|
|
180
|
+
phases = (("format", tool.format), ("fix", unsafe_fix or tool.fix))
|
|
173
181
|
# Leftovers exit non-zero, which is a finding rather than a failure
|
|
174
|
-
for phase in
|
|
175
|
-
configured_command = getattr(tool, phase)
|
|
182
|
+
for phase, configured_command in phases:
|
|
176
183
|
if configured_command:
|
|
177
184
|
commands = _command_batches(
|
|
178
185
|
configured_command, target_count
|
|
@@ -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
|
|
|
@@ -168,8 +167,13 @@ def _echo_languages() -> None:
|
|
|
168
167
|
@click.option(
|
|
169
168
|
"--fix", is_flag=True, help="Automatically fix issues if possible."
|
|
170
169
|
)
|
|
170
|
+
@click.option(
|
|
171
|
+
"--unsafe",
|
|
172
|
+
is_flag=True,
|
|
173
|
+
help="Also apply fixes that may change behavior. Implies --fix.",
|
|
174
|
+
)
|
|
171
175
|
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging.")
|
|
172
|
-
def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
|
|
176
|
+
def check(paths: Sequence[str], fix: bool, unsafe: bool, verbose: bool) -> None:
|
|
173
177
|
"""Run relevant formatters and linters for given paths.
|
|
174
178
|
|
|
175
179
|
Exits with a non-zero status code if any tool reports findings, so the
|
|
@@ -178,7 +182,8 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
|
|
|
178
182
|
if verbose:
|
|
179
183
|
logger.setLevel(logging.DEBUG)
|
|
180
184
|
|
|
181
|
-
|
|
185
|
+
# Unsafe fixes are still fixes, so asking for them is asking to fix
|
|
186
|
+
report = check_paths(paths, fix=fix or unsafe, unsafe=unsafe)
|
|
182
187
|
|
|
183
188
|
# Coverage the caller does not know is missing reads as coverage
|
|
184
189
|
if report.skipped:
|
|
@@ -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 "
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Build executable plans for readability's canonical tools."""
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
|
-
import os
|
|
5
|
-
import shutil
|
|
6
|
-
import tomllib
|
|
7
3
|
from collections.abc import Sequence
|
|
8
4
|
from dataclasses import dataclass
|
|
9
5
|
from importlib.resources import files
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
10
8
|
from pathlib import Path
|
|
9
|
+
import shutil
|
|
10
|
+
import tomllib
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger("readability")
|
|
13
13
|
|
|
@@ -21,6 +21,7 @@ class ToolPlan:
|
|
|
21
21
|
check: tuple[str, ...] = ()
|
|
22
22
|
check_format: tuple[str, ...] = ()
|
|
23
23
|
fix: tuple[str, ...] = ()
|
|
24
|
+
unsafe_fix: tuple[str, ...] = ()
|
|
24
25
|
format: tuple[str, ...] = ()
|
|
25
26
|
targets: tuple[str, ...] | None = None
|
|
26
27
|
cwd: Path | None = None
|
|
@@ -300,10 +301,16 @@ def _plan(
|
|
|
300
301
|
for exclude in excludes
|
|
301
302
|
for argument in (str(flag), str(exclude))
|
|
302
303
|
]
|
|
304
|
+
# Every tool spells its own opt-in, so callers only ask for the intent
|
|
305
|
+
words_by_phase: dict[str, tuple[str, ...]] = dict(TOOL_PHASES[tool])
|
|
306
|
+
unsafe_flag = UNSAFE_FLAGS.get(tool)
|
|
307
|
+
if unsafe_flag and "fix" in words_by_phase:
|
|
308
|
+
words_by_phase["unsafe_fix"] = (*words_by_phase["fix"], unsafe_flag)
|
|
309
|
+
|
|
303
310
|
# Named rather than unpacked, so the type checker still sees the fields
|
|
304
311
|
phases = {
|
|
305
312
|
phase: (*binary, *words, *config, *carved, *argv)
|
|
306
|
-
for phase, words in
|
|
313
|
+
for phase, words in words_by_phase.items()
|
|
307
314
|
}
|
|
308
315
|
return ToolPlan(
|
|
309
316
|
name=tool,
|
|
@@ -313,6 +320,7 @@ def _plan(
|
|
|
313
320
|
check=phases.get("check", ()),
|
|
314
321
|
check_format=phases.get("check_format", ()),
|
|
315
322
|
fix=phases.get("fix", ()),
|
|
323
|
+
unsafe_fix=phases.get("unsafe_fix", ()),
|
|
316
324
|
format=phases.get("format", ()),
|
|
317
325
|
)
|
|
318
326
|
|
|
@@ -386,6 +394,9 @@ TOOL_PHASES = {
|
|
|
386
394
|
"gofmt": {"check_format": ("-l",), "format": ("-w",)},
|
|
387
395
|
}
|
|
388
396
|
|
|
397
|
+
# Per tool: how it names fixes that may change behavior or drop comments
|
|
398
|
+
UNSAFE_FLAGS = {"ruff": "--unsafe-fixes", "biome": "--unsafe"}
|
|
399
|
+
|
|
389
400
|
|
|
390
401
|
def _matching_paths(path: Path, extensions: Sequence[str]) -> list[str]:
|
|
391
402
|
"""Return files under a requested path owned by one tool.
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
import subprocess
|
|
3
4
|
import tomllib
|
|
4
|
-
from
|
|
5
|
-
from unittest.mock import MagicMock
|
|
5
|
+
from unittest.mock import call
|
|
6
|
+
from unittest.mock import MagicMock
|
|
7
|
+
from unittest.mock import patch
|
|
6
8
|
|
|
7
|
-
import pytest
|
|
8
9
|
from click.testing import CliRunner
|
|
10
|
+
import pytest
|
|
9
11
|
|
|
10
|
-
from readability.checking import
|
|
12
|
+
from readability.checking import check_paths
|
|
13
|
+
from readability.checking import CheckReport
|
|
11
14
|
from readability.cli import cli
|
|
12
|
-
from readability.tools import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
_has_project_config,
|
|
17
|
-
)
|
|
15
|
+
from readability.tools import _bundled_config
|
|
16
|
+
from readability.tools import _get_tool_definitions
|
|
17
|
+
from readability.tools import _has_project_config
|
|
18
|
+
from readability.tools import TOOL_RUNNERS
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def _source_file(directory: Path, name: str) -> Path:
|
|
@@ -1035,6 +1036,14 @@ def test_bundled_default_configs_are_valid() -> None:
|
|
|
1035
1036
|
assert ruff_config["line-length"] == 80
|
|
1036
1037
|
assert ruff_config["lint"]["pydocstyle"]["convention"] == "google"
|
|
1037
1038
|
|
|
1039
|
+
# Import sorting mirrors isort's google profile
|
|
1040
|
+
assert ruff_config["lint"]["isort"] == {
|
|
1041
|
+
"force-single-line": True,
|
|
1042
|
+
"force-sort-within-sections": True,
|
|
1043
|
+
"single-line-exclusions": ["typing"],
|
|
1044
|
+
"order-by-type": False,
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1038
1047
|
|
|
1039
1048
|
@patch("readability.checking._check_path")
|
|
1040
1049
|
def test_check_paths_aggregates_str_and_path_inputs(
|
|
@@ -1071,8 +1080,16 @@ def test_check_paths_aggregates_str_and_path_inputs(
|
|
|
1071
1080
|
failed={"biome"},
|
|
1072
1081
|
)
|
|
1073
1082
|
assert mock_check_path.call_args_list == [
|
|
1074
|
-
call(
|
|
1075
|
-
|
|
1083
|
+
call(
|
|
1084
|
+
Path("src/example.py"),
|
|
1085
|
+
project_root,
|
|
1086
|
+
project_root,
|
|
1087
|
+
fix=True,
|
|
1088
|
+
unsafe=False,
|
|
1089
|
+
),
|
|
1090
|
+
call(
|
|
1091
|
+
Path("main.go"), project_root, project_root, fix=True, unsafe=False
|
|
1092
|
+
),
|
|
1076
1093
|
]
|
|
1077
1094
|
|
|
1078
1095
|
|
|
@@ -1119,7 +1136,7 @@ def test_check_paths_defaults_project_root_to_cwd(
|
|
|
1119
1136
|
check_paths([Path("example.py")])
|
|
1120
1137
|
|
|
1121
1138
|
mock_check_path.assert_called_once_with(
|
|
1122
|
-
Path("example.py"), tmp_path, None, fix=False
|
|
1139
|
+
Path("example.py"), tmp_path, None, fix=False, unsafe=False
|
|
1123
1140
|
)
|
|
1124
1141
|
|
|
1125
1142
|
|
|
@@ -1188,7 +1205,9 @@ def test_check_command_delegates_to_public_api(
|
|
|
1188
1205
|
Path("script.py").touch()
|
|
1189
1206
|
result = runner.invoke(cli, ["check", "script.py"])
|
|
1190
1207
|
|
|
1191
|
-
mock_check_paths.assert_called_once_with(
|
|
1208
|
+
mock_check_paths.assert_called_once_with(
|
|
1209
|
+
("script.py",), fix=False, unsafe=False
|
|
1210
|
+
)
|
|
1192
1211
|
assert result.exit_code == 0
|
|
1193
1212
|
assert result.stdout == ""
|
|
1194
1213
|
assert result.stderr == "No findings in 1 path(s) (ruff).\n"
|
|
@@ -2043,3 +2062,136 @@ def test_fix_still_reports_what_it_could_not_fix(
|
|
|
2043
2062
|
commands = [call.args[0] for call in mock_run.call_args_list]
|
|
2044
2063
|
assert any("--fix" not in c and "check" in c for c in commands)
|
|
2045
2064
|
assert result.exit_code != 0
|
|
2065
|
+
|
|
2066
|
+
|
|
2067
|
+
@patch("shutil.which")
|
|
2068
|
+
@patch("subprocess.run")
|
|
2069
|
+
def test_unsafe_translates_to_the_ruff_flag(
|
|
2070
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2071
|
+
) -> None:
|
|
2072
|
+
"""--unsafe reaches ruff under the name ruff gives it.
|
|
2073
|
+
|
|
2074
|
+
Args:
|
|
2075
|
+
mock_run: The mocked subprocess.run function.
|
|
2076
|
+
mock_which: The mocked shutil.which function.
|
|
2077
|
+
tmp_path: The temporary directory fixture.
|
|
2078
|
+
"""
|
|
2079
|
+
mock_which.side_effect = lambda name: name if name == "ruff" else None
|
|
2080
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2081
|
+
|
|
2082
|
+
runner = CliRunner()
|
|
2083
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2084
|
+
Path("pyproject.toml").touch()
|
|
2085
|
+
Path("script.py").touch()
|
|
2086
|
+
|
|
2087
|
+
result = runner.invoke(cli, ["check", "--fix", "--unsafe", "script.py"])
|
|
2088
|
+
|
|
2089
|
+
assert result.exit_code == 0
|
|
2090
|
+
cfg = str(_bundled_config("ruff"))
|
|
2091
|
+
called_cmds = [call.args[0] for call in mock_run.call_args_list]
|
|
2092
|
+
assert [
|
|
2093
|
+
"ruff",
|
|
2094
|
+
"check",
|
|
2095
|
+
"--fix",
|
|
2096
|
+
"--force-exclude",
|
|
2097
|
+
"--unsafe-fixes",
|
|
2098
|
+
"--config",
|
|
2099
|
+
cfg,
|
|
2100
|
+
"script.py",
|
|
2101
|
+
] in called_cmds
|
|
2102
|
+
# Formatters have no unsafe mode, so the flag must not reach them
|
|
2103
|
+
assert [
|
|
2104
|
+
"ruff",
|
|
2105
|
+
"format",
|
|
2106
|
+
"--force-exclude",
|
|
2107
|
+
"--config",
|
|
2108
|
+
cfg,
|
|
2109
|
+
"script.py",
|
|
2110
|
+
] in called_cmds
|
|
2111
|
+
|
|
2112
|
+
|
|
2113
|
+
@patch("shutil.which")
|
|
2114
|
+
@patch("subprocess.run")
|
|
2115
|
+
def test_unsafe_translates_to_the_biome_flag(
|
|
2116
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2117
|
+
) -> None:
|
|
2118
|
+
"""Biome spells the same intent differently and gets its own spelling.
|
|
2119
|
+
|
|
2120
|
+
Args:
|
|
2121
|
+
mock_run: The mocked subprocess.run function.
|
|
2122
|
+
mock_which: The mocked shutil.which function.
|
|
2123
|
+
tmp_path: The temporary directory fixture.
|
|
2124
|
+
"""
|
|
2125
|
+
mock_which.side_effect = lambda name: name if name == "biome" else None
|
|
2126
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2127
|
+
|
|
2128
|
+
runner = CliRunner()
|
|
2129
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2130
|
+
Path("script.ts").touch()
|
|
2131
|
+
|
|
2132
|
+
result = runner.invoke(cli, ["check", "--fix", "--unsafe", "script.ts"])
|
|
2133
|
+
|
|
2134
|
+
assert result.exit_code == 0
|
|
2135
|
+
called_cmds = [call.args[0] for call in mock_run.call_args_list]
|
|
2136
|
+
lint = next(c for c in called_cmds if "lint" in c)
|
|
2137
|
+
assert lint[:5] == [
|
|
2138
|
+
"biome",
|
|
2139
|
+
"lint",
|
|
2140
|
+
"--write",
|
|
2141
|
+
"--no-errors-on-unmatched",
|
|
2142
|
+
"--unsafe",
|
|
2143
|
+
]
|
|
2144
|
+
|
|
2145
|
+
|
|
2146
|
+
@patch("shutil.which")
|
|
2147
|
+
@patch("subprocess.run")
|
|
2148
|
+
def test_unsafe_implies_fix(
|
|
2149
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2150
|
+
) -> None:
|
|
2151
|
+
"""Asking for unsafe fixes without --fix asks for nothing otherwise.
|
|
2152
|
+
|
|
2153
|
+
Args:
|
|
2154
|
+
mock_run: The mocked subprocess.run function.
|
|
2155
|
+
mock_which: The mocked shutil.which function.
|
|
2156
|
+
tmp_path: The temporary directory fixture.
|
|
2157
|
+
"""
|
|
2158
|
+
mock_which.side_effect = lambda name: name if name == "ruff" else None
|
|
2159
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2160
|
+
|
|
2161
|
+
runner = CliRunner()
|
|
2162
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2163
|
+
Path("pyproject.toml").touch()
|
|
2164
|
+
Path("script.py").touch()
|
|
2165
|
+
|
|
2166
|
+
result = runner.invoke(cli, ["check", "--unsafe", "script.py"])
|
|
2167
|
+
|
|
2168
|
+
assert result.exit_code == 0
|
|
2169
|
+
called_cmds = [call.args[0] for call in mock_run.call_args_list]
|
|
2170
|
+
assert any("--unsafe-fixes" in command for command in called_cmds)
|
|
2171
|
+
|
|
2172
|
+
|
|
2173
|
+
@patch("shutil.which")
|
|
2174
|
+
@patch("subprocess.run")
|
|
2175
|
+
def test_unsafe_is_a_no_op_for_tools_without_one(
|
|
2176
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2177
|
+
) -> None:
|
|
2178
|
+
"""A tool with no unsafe mode still fixes rather than being skipped.
|
|
2179
|
+
|
|
2180
|
+
Args:
|
|
2181
|
+
mock_run: The mocked subprocess.run function.
|
|
2182
|
+
mock_which: The mocked shutil.which function.
|
|
2183
|
+
tmp_path: The temporary directory fixture.
|
|
2184
|
+
"""
|
|
2185
|
+
mock_which.side_effect = lambda name: name if name == "gofmt" else None
|
|
2186
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2187
|
+
|
|
2188
|
+
runner = CliRunner()
|
|
2189
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2190
|
+
Path("main.go").touch()
|
|
2191
|
+
|
|
2192
|
+
result = runner.invoke(cli, ["check", "--fix", "--unsafe", "main.go"])
|
|
2193
|
+
|
|
2194
|
+
assert result.exit_code == 0
|
|
2195
|
+
assert [invocation.args[0] for invocation in mock_run.call_args_list] == [
|
|
2196
|
+
["gofmt", "-w", "main.go"]
|
|
2197
|
+
]
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from unittest.mock import MagicMock
|
|
3
|
+
from unittest.mock import MagicMock
|
|
4
|
+
from unittest.mock import patch
|
|
4
5
|
|
|
5
6
|
import click
|
|
7
|
+
from click.testing import CliRunner
|
|
6
8
|
import pytest
|
|
7
9
|
import requests
|
|
8
|
-
from click.testing import CliRunner
|
|
9
10
|
|
|
10
11
|
from readability.cli import cli
|
|
11
|
-
from readability.guide import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
get_local_path,
|
|
17
|
-
)
|
|
12
|
+
from readability.guide import convert_to_markdown
|
|
13
|
+
from readability.guide import get_guide
|
|
14
|
+
from readability.guide import get_guide_content
|
|
15
|
+
from readability.guide import get_guides_dir
|
|
16
|
+
from readability.guide import get_local_path
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
def test_get_guide_unsupported() -> None:
|
|
@@ -159,8 +158,7 @@ def test_sync_command(
|
|
|
159
158
|
result = runner.invoke(cli, ["sync"])
|
|
160
159
|
|
|
161
160
|
assert result.exit_code == 0
|
|
162
|
-
#
|
|
163
|
-
# regardless of log level rather than logged as narration.
|
|
161
|
+
# An asked-for outcome, so reported regardless of log level
|
|
164
162
|
assert "Sync complete" in result.stderr
|
|
165
163
|
# Check if at least one guide was "synced" (written to tmp_path)
|
|
166
164
|
assert len(os.listdir(tmp_path)) > 0
|
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from unittest.mock import MagicMock
|
|
4
|
+
from unittest.mock import MagicMock
|
|
5
|
+
from unittest.mock import patch
|
|
5
6
|
|
|
6
7
|
from click.testing import CliRunner
|
|
7
8
|
|
|
8
|
-
from readability.cli import cli
|
|
9
|
-
from readability.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
from readability.outline import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
find_mentions,
|
|
19
|
-
parse_headings,
|
|
20
|
-
)
|
|
9
|
+
from readability.cli import cli
|
|
10
|
+
from readability.cli import main
|
|
11
|
+
from readability.guide import get_local_path
|
|
12
|
+
from readability.guide import LANGUAGE_MAP
|
|
13
|
+
from readability.outline import _iter_heading_lines
|
|
14
|
+
from readability.outline import _unique_reference
|
|
15
|
+
from readability.outline import extract_section
|
|
16
|
+
from readability.outline import find_headings
|
|
17
|
+
from readability.outline import find_mentions
|
|
18
|
+
from readability.outline import parse_headings
|
|
21
19
|
|
|
22
20
|
NUMBERED_GUIDE = """# Sample Style Guide
|
|
23
21
|
|
|
@@ -456,10 +454,7 @@ def test_cli_section_heading_containing_an_angle_bracket(
|
|
|
456
454
|
assert "Array body." in result.stdout
|
|
457
455
|
|
|
458
456
|
|
|
459
|
-
#
|
|
460
|
-
# _iter_heading_lines). These tests are the evidence for that decision: the
|
|
461
|
-
# scope it covers, the constructs it rejects, and its behaviour over every
|
|
462
|
-
# guide actually shipped. A failure here means the scope has been outgrown.
|
|
457
|
+
# Evidence the scanner need not be a Markdown parser; a failure means it is
|
|
463
458
|
|
|
464
459
|
|
|
465
460
|
def test_heading_scanner_rejects_non_headings() -> None:
|
|
@@ -551,8 +546,7 @@ def test_every_shipped_guide_parses_into_unique_sections() -> None:
|
|
|
551
546
|
f"{language}: duplicate positional indices"
|
|
552
547
|
)
|
|
553
548
|
|
|
554
|
-
# A
|
|
555
|
-
# or a shebang, neither of which appears in a real guide heading.
|
|
549
|
+
# A fenced line would carry a comment marker or a shebang
|
|
556
550
|
for heading in headings:
|
|
557
551
|
assert not heading.title.startswith("!"), (
|
|
558
552
|
f"{language}: shebang parsed as heading: {heading.title!r}"
|
|
@@ -575,9 +569,7 @@ def test_every_shipped_guide_heading_is_addressable() -> None:
|
|
|
575
569
|
)
|
|
576
570
|
assert titles[0] is headings[0], f"{language}: title is not first"
|
|
577
571
|
|
|
578
|
-
#
|
|
579
|
-
# numbers those drift from the tree, so one string can name two
|
|
580
|
-
# headings. What must always hold is that some reference resolves.
|
|
572
|
+
# A guide's own numbers drift, so only 'some reference resolves' holds
|
|
581
573
|
for position, heading in enumerate(headings):
|
|
582
574
|
reference = _unique_reference(headings, position).strip('"')
|
|
583
575
|
matches = find_headings(headings, reference)
|
|
@@ -587,9 +579,7 @@ def test_every_shipped_guide_heading_is_addressable() -> None:
|
|
|
587
579
|
)
|
|
588
580
|
|
|
589
581
|
|
|
590
|
-
#
|
|
591
|
-
# 'truefalse' appears only inside pyguide's anchors, which is the shape that
|
|
592
|
-
# used to be credited to whichever section happened to precede them.
|
|
582
|
+
# Common words, plus 'truefalse' which appears only inside pyguide's anchors
|
|
593
583
|
MENTION_PROBES = (
|
|
594
584
|
"truefalse",
|
|
595
585
|
"indent",
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from importlib.resources import files
|
|
2
2
|
|
|
3
3
|
import readability
|
|
4
|
-
from readability.checking import
|
|
5
|
-
from readability.
|
|
4
|
+
from readability.checking import check_paths
|
|
5
|
+
from readability.checking import CheckReport
|
|
6
|
+
from readability.cli import cli
|
|
7
|
+
from readability.cli import main
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
def test_public_modules_are_importable() -> None:
|
|
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
|