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.
Files changed (28) hide show
  1. {readability_cli-0.8.0 → readability_cli-0.8.2}/.github/workflows/ci.yml +9 -0
  2. {readability_cli-0.8.0 → readability_cli-0.8.2}/PKG-INFO +23 -1
  3. {readability_cli-0.8.0 → readability_cli-0.8.2}/README.md +22 -0
  4. {readability_cli-0.8.0 → readability_cli-0.8.2}/pyproject.toml +1 -1
  5. {readability_cli-0.8.0 → readability_cli-0.8.2}/readability.py +41 -8
  6. {readability_cli-0.8.0 → readability_cli-0.8.2}/test_readability.py +131 -1
  7. {readability_cli-0.8.0 → readability_cli-0.8.2}/uv.lock +1 -1
  8. {readability_cli-0.8.0 → readability_cli-0.8.2}/.github/workflows/publish.yml +0 -0
  9. {readability_cli-0.8.0 → readability_cli-0.8.2}/.github/workflows/update-guides.yml +0 -0
  10. {readability_cli-0.8.0 → readability_cli-0.8.2}/.gitignore +0 -0
  11. {readability_cli-0.8.0 → readability_cli-0.8.2}/.python-version +0 -0
  12. {readability_cli-0.8.0 → readability_cli-0.8.2}/LICENSE +0 -0
  13. {readability_cli-0.8.0 → readability_cli-0.8.2}/configs/pyrefly.toml +0 -0
  14. {readability_cli-0.8.0 → readability_cli-0.8.2}/configs/ruff.toml +0 -0
  15. {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/Rguide.md +0 -0
  16. {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/cppguide.md +0 -0
  17. {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/csharp-style.md +0 -0
  18. {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/docguide-style.md +0 -0
  19. {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/go-guide.md +0 -0
  20. {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/htmlcssguide.md +0 -0
  21. {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/javaguide.md +0 -0
  22. {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/jsguide.md +0 -0
  23. {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/jsoncstyleguide.md +0 -0
  24. {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/objcguide.md +0 -0
  25. {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/pyguide.md +0 -0
  26. {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/shellguide.md +0 -0
  27. {readability_cli-0.8.0 → readability_cli-0.8.2}/guides/tsguide.md +0 -0
  28. {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.0
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "readability-cli"
3
- version = "0.8.0"
3
+ version = "0.8.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"
@@ -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
- # Resolve project root once for trigger file checking
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(
@@ -194,7 +194,7 @@ wheels = [
194
194
 
195
195
  [[package]]
196
196
  name = "readability-cli"
197
- version = "0.8.0"
197
+ version = "0.8.2"
198
198
  source = { editable = "." }
199
199
  dependencies = [
200
200
  { name = "beautifulsoup4" },
File without changes