readability-cli 0.8.0__tar.gz → 0.8.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.8.0 → readability_cli-0.8.2}/.github/workflows/ci.yml +9 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/PKG-INFO +23 -1
- {readability_cli-0.8.0 → readability_cli-0.8.2}/README.md +22 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/pyproject.toml +1 -1
- {readability_cli-0.8.0 → readability_cli-0.8.2}/readability.py +41 -8
- {readability_cli-0.8.0 → readability_cli-0.8.2}/test_readability.py +131 -1
- {readability_cli-0.8.0 → readability_cli-0.8.2}/uv.lock +1 -1
- {readability_cli-0.8.0 → readability_cli-0.8.2}/.github/workflows/publish.yml +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/.github/workflows/update-guides.yml +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/.gitignore +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/.python-version +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/LICENSE +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/configs/pyrefly.toml +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/configs/ruff.toml +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/Rguide.md +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/cppguide.md +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/csharp-style.md +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/docguide-style.md +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/go-guide.md +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/htmlcssguide.md +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/javaguide.md +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/jsguide.md +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/jsoncstyleguide.md +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/objcguide.md +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/pyguide.md +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/shellguide.md +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/tsguide.md +0 -0
- {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/vimscriptguide.md +0 -0
|
@@ -29,3 +29,12 @@ jobs:
|
|
|
29
29
|
|
|
30
30
|
- name: Run tests
|
|
31
31
|
run: uv run pytest
|
|
32
|
+
|
|
33
|
+
# Runs the tool against itself, which is the only place tool
|
|
34
|
+
# resolution is exercised for real: the unit tests reach it with
|
|
35
|
+
# shutil.which mocked, so they cannot tell whether a resolved command
|
|
36
|
+
# actually starts. ruff comes from the synced environment and pyrefly
|
|
37
|
+
# is not a dependency at all, so this covers both the PATH branch and
|
|
38
|
+
# the uvx runner, on a machine that has neither installed.
|
|
39
|
+
- name: Check this repository with the tool itself
|
|
40
|
+
run: uv run readability check .
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: readability-cli
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -147,6 +147,28 @@ the state a container image is usually in.
|
|
|
147
147
|
`check` exits non-zero on findings and on having verified nothing, so it can
|
|
148
148
|
gate CI without a clean exit ever meaning "the tools were absent".
|
|
149
149
|
|
|
150
|
+
### Python API
|
|
151
|
+
|
|
152
|
+
Use `check_paths` to run the same checks without the command's status messages
|
|
153
|
+
or exit-code decisions:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from pathlib import Path
|
|
157
|
+
|
|
158
|
+
from readability import check_paths
|
|
159
|
+
|
|
160
|
+
report = check_paths(["src", Path("tests")], project_root=Path.cwd())
|
|
161
|
+
if report.findings or report.failed or not report.ran:
|
|
162
|
+
handle_failed_check(report)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The returned `CheckReport` records only whether findings occurred and which
|
|
166
|
+
tools ran, were skipped, or failed. Detailed tool findings are still written
|
|
167
|
+
as each tool runs. Every path is validated before any tool runs, and a missing
|
|
168
|
+
one raises `FileNotFoundError` rather than being misreported as a finding.
|
|
169
|
+
Relative paths remain relative to the process working directory;
|
|
170
|
+
`project_root` controls configuration discovery only.
|
|
171
|
+
|
|
150
172
|
### Default Configurations
|
|
151
173
|
|
|
152
174
|
For Ruff and Pyrefly, bundled defaults based on the
|
|
@@ -133,6 +133,28 @@ the state a container image is usually in.
|
|
|
133
133
|
`check` exits non-zero on findings and on having verified nothing, so it can
|
|
134
134
|
gate CI without a clean exit ever meaning "the tools were absent".
|
|
135
135
|
|
|
136
|
+
### Python API
|
|
137
|
+
|
|
138
|
+
Use `check_paths` to run the same checks without the command's status messages
|
|
139
|
+
or exit-code decisions:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from pathlib import Path
|
|
143
|
+
|
|
144
|
+
from readability import check_paths
|
|
145
|
+
|
|
146
|
+
report = check_paths(["src", Path("tests")], project_root=Path.cwd())
|
|
147
|
+
if report.findings or report.failed or not report.ran:
|
|
148
|
+
handle_failed_check(report)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The returned `CheckReport` records only whether findings occurred and which
|
|
152
|
+
tools ran, were skipped, or failed. Detailed tool findings are still written
|
|
153
|
+
as each tool runs. Every path is validated before any tool runs, and a missing
|
|
154
|
+
one raises `FileNotFoundError` rather than being misreported as a finding.
|
|
155
|
+
Relative paths remain relative to the process working directory;
|
|
156
|
+
`project_root` controls configuration discovery only.
|
|
157
|
+
|
|
136
158
|
### Default Configurations
|
|
137
159
|
|
|
138
160
|
For Ruff and Pyrefly, bundled defaults based on the
|
|
@@ -1096,14 +1096,7 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
|
|
|
1096
1096
|
if verbose:
|
|
1097
1097
|
logger.setLevel(logging.DEBUG)
|
|
1098
1098
|
|
|
1099
|
-
|
|
1100
|
-
project_root = Path.cwd()
|
|
1101
|
-
|
|
1102
|
-
# Process each provided path independently, tracking findings across
|
|
1103
|
-
# all of them so the exit code reflects the overall result
|
|
1104
|
-
report = CheckReport()
|
|
1105
|
-
for path_str in paths:
|
|
1106
|
-
report.absorb(_check_path(Path(path_str), project_root, fix=fix))
|
|
1099
|
+
report = check_paths(paths, fix=fix)
|
|
1107
1100
|
|
|
1108
1101
|
# Coverage the caller does not know is missing reads as coverage
|
|
1109
1102
|
if report.skipped:
|
|
@@ -1190,6 +1183,46 @@ class CheckReport:
|
|
|
1190
1183
|
self.failed |= other.failed
|
|
1191
1184
|
|
|
1192
1185
|
|
|
1186
|
+
def check_paths(
|
|
1187
|
+
paths: Sequence[str | Path],
|
|
1188
|
+
project_root: Path | None = None,
|
|
1189
|
+
fix: bool = False,
|
|
1190
|
+
) -> CheckReport:
|
|
1191
|
+
"""Run relevant checks for paths and aggregate what the tools did.
|
|
1192
|
+
|
|
1193
|
+
Detailed findings from the underlying tools are written as they are for
|
|
1194
|
+
the command-line interface. This function does not print CLI status prose
|
|
1195
|
+
or turn the report into a process exit code; callers decide how to handle
|
|
1196
|
+
the result.
|
|
1197
|
+
|
|
1198
|
+
Args:
|
|
1199
|
+
paths: Files or directories to check, as strings or paths. Relative
|
|
1200
|
+
paths are interpreted from the current working directory.
|
|
1201
|
+
project_root: Root used only to discover tool configuration. Defaults
|
|
1202
|
+
to the current working directory; it does not rebase paths.
|
|
1203
|
+
fix: Whether to apply automatic fixes.
|
|
1204
|
+
|
|
1205
|
+
Returns:
|
|
1206
|
+
A report aggregated across all provided paths.
|
|
1207
|
+
|
|
1208
|
+
Raises:
|
|
1209
|
+
FileNotFoundError: If any requested path does not exist. Every path is
|
|
1210
|
+
validated before any tools run.
|
|
1211
|
+
"""
|
|
1212
|
+
root = Path.cwd() if project_root is None else project_root
|
|
1213
|
+
requested_paths = [Path(path) for path in paths]
|
|
1214
|
+
missing_path = next(
|
|
1215
|
+
(path for path in requested_paths if not path.exists()), None
|
|
1216
|
+
)
|
|
1217
|
+
if missing_path is not None:
|
|
1218
|
+
raise FileNotFoundError(f"Path does not exist: {missing_path}")
|
|
1219
|
+
|
|
1220
|
+
report = CheckReport()
|
|
1221
|
+
for path in requested_paths:
|
|
1222
|
+
report.absorb(_check_path(path, root, fix=fix))
|
|
1223
|
+
return report
|
|
1224
|
+
|
|
1225
|
+
|
|
1193
1226
|
def _check_path(
|
|
1194
1227
|
path: Path, project_root: Path, fix: bool = False
|
|
1195
1228
|
) -> CheckReport:
|
|
@@ -3,7 +3,7 @@ import os
|
|
|
3
3
|
import subprocess
|
|
4
4
|
import tomllib
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from unittest.mock import MagicMock, patch
|
|
6
|
+
from unittest.mock import MagicMock, call, patch
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
9
|
import pytest
|
|
@@ -13,11 +13,13 @@ from click.testing import CliRunner
|
|
|
13
13
|
from readability import (
|
|
14
14
|
LANGUAGE_MAP,
|
|
15
15
|
TOOL_RUNNERS,
|
|
16
|
+
CheckReport,
|
|
16
17
|
_bundled_config,
|
|
17
18
|
_get_tool_definitions,
|
|
18
19
|
_has_project_config,
|
|
19
20
|
_iter_heading_lines,
|
|
20
21
|
_unique_reference,
|
|
22
|
+
check_paths,
|
|
21
23
|
cli,
|
|
22
24
|
convert_to_markdown,
|
|
23
25
|
extract_section,
|
|
@@ -518,6 +520,134 @@ def test_bundled_default_configs_are_valid(tmp_path: Path) -> None:
|
|
|
518
520
|
assert ruff_config["lint"]["pydocstyle"]["convention"] == "google"
|
|
519
521
|
|
|
520
522
|
|
|
523
|
+
@patch("readability._check_path")
|
|
524
|
+
def test_check_paths_aggregates_str_and_path_inputs(
|
|
525
|
+
mock_check_path: MagicMock,
|
|
526
|
+
tmp_path: Path,
|
|
527
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
528
|
+
) -> None:
|
|
529
|
+
"""The public API folds each per-path result into one limited report."""
|
|
530
|
+
project_root = tmp_path / "project"
|
|
531
|
+
project_root.mkdir()
|
|
532
|
+
(tmp_path / "src").mkdir()
|
|
533
|
+
(tmp_path / "src" / "example.py").touch()
|
|
534
|
+
(tmp_path / "README.md").touch()
|
|
535
|
+
monkeypatch.chdir(tmp_path)
|
|
536
|
+
mock_check_path.side_effect = [
|
|
537
|
+
CheckReport(ran={"ruff"}, skipped={"pyrefly"}),
|
|
538
|
+
CheckReport(
|
|
539
|
+
findings=True,
|
|
540
|
+
ran={"prettier"},
|
|
541
|
+
failed={"biome"},
|
|
542
|
+
),
|
|
543
|
+
]
|
|
544
|
+
|
|
545
|
+
report = check_paths(
|
|
546
|
+
["src/example.py", Path("README.md")],
|
|
547
|
+
project_root=project_root,
|
|
548
|
+
fix=True,
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
assert report == CheckReport(
|
|
552
|
+
findings=True,
|
|
553
|
+
ran={"ruff", "prettier"},
|
|
554
|
+
skipped={"pyrefly"},
|
|
555
|
+
failed={"biome"},
|
|
556
|
+
)
|
|
557
|
+
assert mock_check_path.call_args_list == [
|
|
558
|
+
call(Path("src/example.py"), project_root, fix=True),
|
|
559
|
+
call(Path("README.md"), project_root, fix=True),
|
|
560
|
+
]
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@patch("readability._check_path", return_value=CheckReport(ran={"ruff"}))
|
|
564
|
+
def test_check_paths_defaults_project_root_to_cwd(
|
|
565
|
+
mock_check_path: MagicMock,
|
|
566
|
+
tmp_path: Path,
|
|
567
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
568
|
+
) -> None:
|
|
569
|
+
"""The public API discovers configuration from the caller's directory."""
|
|
570
|
+
monkeypatch.chdir(tmp_path)
|
|
571
|
+
(tmp_path / "example.py").touch()
|
|
572
|
+
|
|
573
|
+
check_paths([Path("example.py")])
|
|
574
|
+
|
|
575
|
+
mock_check_path.assert_called_once_with(
|
|
576
|
+
Path("example.py"), tmp_path, fix=False
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
@patch("readability._check_path")
|
|
581
|
+
def test_check_paths_with_no_paths_reports_that_nothing_ran(
|
|
582
|
+
mock_check_path: MagicMock,
|
|
583
|
+
) -> None:
|
|
584
|
+
"""An empty request must not look like a verified clean result."""
|
|
585
|
+
report = check_paths([])
|
|
586
|
+
|
|
587
|
+
assert report == CheckReport()
|
|
588
|
+
assert not report.ran
|
|
589
|
+
mock_check_path.assert_not_called()
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
@patch("readability._check_path")
|
|
593
|
+
def test_check_paths_rejects_missing_paths_before_dispatch(
|
|
594
|
+
mock_check_path: MagicMock,
|
|
595
|
+
tmp_path: Path,
|
|
596
|
+
) -> None:
|
|
597
|
+
"""A missing path prevents partial checks and fixes on earlier paths."""
|
|
598
|
+
existing = tmp_path / "exists.py"
|
|
599
|
+
missing = tmp_path / "missing.py"
|
|
600
|
+
existing.touch()
|
|
601
|
+
|
|
602
|
+
with pytest.raises(FileNotFoundError) as excinfo:
|
|
603
|
+
check_paths([existing, missing], project_root=tmp_path, fix=True)
|
|
604
|
+
|
|
605
|
+
assert str(missing) in str(excinfo.value)
|
|
606
|
+
mock_check_path.assert_not_called()
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
@patch(
|
|
610
|
+
"readability._check_path",
|
|
611
|
+
return_value=CheckReport(
|
|
612
|
+
findings=True,
|
|
613
|
+
skipped={"pyrefly"},
|
|
614
|
+
failed={"ruff"},
|
|
615
|
+
),
|
|
616
|
+
)
|
|
617
|
+
def test_check_paths_returns_report_without_cli_status_prose(
|
|
618
|
+
mock_check_path: MagicMock,
|
|
619
|
+
capsys: pytest.CaptureFixture[str],
|
|
620
|
+
tmp_path: Path,
|
|
621
|
+
) -> None:
|
|
622
|
+
"""Library callers receive report states without CLI policy or summaries."""
|
|
623
|
+
path = tmp_path / "example.py"
|
|
624
|
+
path.touch()
|
|
625
|
+
|
|
626
|
+
report = check_paths([path])
|
|
627
|
+
|
|
628
|
+
assert report.findings is True
|
|
629
|
+
assert report.skipped == {"pyrefly"}
|
|
630
|
+
assert report.failed == {"ruff"}
|
|
631
|
+
assert capsys.readouterr() == ("", "")
|
|
632
|
+
mock_check_path.assert_called_once()
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
@patch("readability.check_paths", return_value=CheckReport(ran={"ruff"}))
|
|
636
|
+
def test_check_command_delegates_to_public_api(
|
|
637
|
+
mock_check_paths: MagicMock, tmp_path: Path
|
|
638
|
+
) -> None:
|
|
639
|
+
"""The CLI keeps its output policy while delegating check execution."""
|
|
640
|
+
runner = CliRunner()
|
|
641
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
642
|
+
Path("script.py").touch()
|
|
643
|
+
result = runner.invoke(cli, ["check", "script.py"])
|
|
644
|
+
|
|
645
|
+
mock_check_paths.assert_called_once_with(("script.py",), fix=False)
|
|
646
|
+
assert result.exit_code == 0
|
|
647
|
+
assert result.stdout == ""
|
|
648
|
+
assert result.stderr == "No findings in 1 path(s) (ruff).\n"
|
|
649
|
+
|
|
650
|
+
|
|
521
651
|
@patch("shutil.which")
|
|
522
652
|
@patch("subprocess.run")
|
|
523
653
|
def test_check_command_exits_nonzero_on_format_findings(
|
|
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
|