readability-cli 0.4.0__tar.gz → 0.6.0__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.4.0 → readability_cli-0.6.0}/PKG-INFO +28 -5
  2. {readability_cli-0.4.0 → readability_cli-0.6.0}/README.md +26 -3
  3. {readability_cli-0.4.0 → readability_cli-0.6.0}/pyproject.toml +1 -1
  4. {readability_cli-0.4.0 → readability_cli-0.6.0}/readability.py +82 -9
  5. {readability_cli-0.4.0 → readability_cli-0.6.0}/test_readability.py +150 -0
  6. {readability_cli-0.4.0 → readability_cli-0.6.0}/uv.lock +1 -1
  7. {readability_cli-0.4.0 → readability_cli-0.6.0}/.github/workflows/ci.yml +0 -0
  8. {readability_cli-0.4.0 → readability_cli-0.6.0}/.github/workflows/publish.yml +0 -0
  9. {readability_cli-0.4.0 → readability_cli-0.6.0}/.github/workflows/update-guides.yml +0 -0
  10. {readability_cli-0.4.0 → readability_cli-0.6.0}/.gitignore +0 -0
  11. {readability_cli-0.4.0 → readability_cli-0.6.0}/.python-version +0 -0
  12. {readability_cli-0.4.0 → readability_cli-0.6.0}/LICENSE +0 -0
  13. {readability_cli-0.4.0 → readability_cli-0.6.0}/configs/pyrefly.toml +0 -0
  14. {readability_cli-0.4.0 → readability_cli-0.6.0}/configs/ruff.toml +0 -0
  15. {readability_cli-0.4.0 → readability_cli-0.6.0}/guides/Rguide.md +0 -0
  16. {readability_cli-0.4.0 → readability_cli-0.6.0}/guides/cppguide.md +0 -0
  17. {readability_cli-0.4.0 → readability_cli-0.6.0}/guides/csharp-style.md +0 -0
  18. {readability_cli-0.4.0 → readability_cli-0.6.0}/guides/docguide-style.md +0 -0
  19. {readability_cli-0.4.0 → readability_cli-0.6.0}/guides/go-guide.md +0 -0
  20. {readability_cli-0.4.0 → readability_cli-0.6.0}/guides/htmlcssguide.md +0 -0
  21. {readability_cli-0.4.0 → readability_cli-0.6.0}/guides/javaguide.md +0 -0
  22. {readability_cli-0.4.0 → readability_cli-0.6.0}/guides/jsguide.md +0 -0
  23. {readability_cli-0.4.0 → readability_cli-0.6.0}/guides/jsoncstyleguide.md +0 -0
  24. {readability_cli-0.4.0 → readability_cli-0.6.0}/guides/objcguide.md +0 -0
  25. {readability_cli-0.4.0 → readability_cli-0.6.0}/guides/pyguide.md +0 -0
  26. {readability_cli-0.4.0 → readability_cli-0.6.0}/guides/shellguide.md +0 -0
  27. {readability_cli-0.4.0 → readability_cli-0.6.0}/guides/tsguide.md +0 -0
  28. {readability_cli-0.4.0 → readability_cli-0.6.0}/guides/vimscriptguide.md +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: readability-cli
3
- Version: 0.4.0
3
+ Version: 0.6.0
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
@@ -37,10 +37,10 @@ You can run the tool directly without installing it using `uvx`:
37
37
 
38
38
  ```bash
39
39
  # Check and fix formatting for the current directory
40
- uvx --from git+https://github.com/owahltinez/readability.git readability check . --fix
40
+ uvx --from readability-cli readability check . --fix
41
41
 
42
42
  # Get the Python style guide
43
- uvx --from git+https://github.com/owahltinez/readability.git readability guide python
43
+ uvx --from readability-cli readability guide python
44
44
  ```
45
45
 
46
46
  ## Installation
@@ -49,7 +49,7 @@ Install it as a global tool with `uv`:
49
49
 
50
50
  ```bash
51
51
  # Install the readability tool
52
- uv tool install git+https://github.com/owahltinez/readability.git
52
+ uv tool install readability-cli
53
53
 
54
54
  # Use it anywhere
55
55
  readability check .
@@ -128,10 +128,19 @@ readability guide python --remote
128
128
  # Save a style guide to a file
129
129
  readability guide cpp --output cpp-style.md
130
130
 
131
+ # Print where the guide already lives, to search it without copying it
132
+ readability guide python --path
133
+ grep -n "Naming" "$(readability guide python --path)"
134
+
131
135
  # Synchronize all supported style guides to the local cache
132
136
  readability sync
133
137
  ```
134
138
 
139
+ A guide can exceed 100 KB, so `--path` is the cheap way to consult one: the
140
+ file is already on disk, and searching it beats reading it whole or writing
141
+ out a second copy. It is also what a coding agent should use, rather than
142
+ redirecting the guide into a repository.
143
+
135
144
  ### Supported Languages
136
145
 
137
146
  Use `readability languages` to see a full list of supported languages and
@@ -190,3 +199,17 @@ Check code style with `ruff`:
190
199
  uv run ruff check .
191
200
  uv run ruff format .
192
201
  ```
202
+
203
+ ### Releasing
204
+
205
+ Releases are published to PyPI as
206
+ [`readability-cli`](https://pypi.org/project/readability-cli/) via trusted
207
+ publishing: pushing a `v*` tag triggers the `publish.yml` GitHub Actions
208
+ workflow, which builds the package with `uv build` and uploads it.
209
+
210
+ ```bash
211
+ # 1. Bump the version in pyproject.toml, commit, and push
212
+ # 2. Tag the release and push the tag
213
+ git tag v0.4.1
214
+ git push origin v0.4.1
215
+ ```
@@ -23,10 +23,10 @@ You can run the tool directly without installing it using `uvx`:
23
23
 
24
24
  ```bash
25
25
  # Check and fix formatting for the current directory
26
- uvx --from git+https://github.com/owahltinez/readability.git readability check . --fix
26
+ uvx --from readability-cli readability check . --fix
27
27
 
28
28
  # Get the Python style guide
29
- uvx --from git+https://github.com/owahltinez/readability.git readability guide python
29
+ uvx --from readability-cli readability guide python
30
30
  ```
31
31
 
32
32
  ## Installation
@@ -35,7 +35,7 @@ Install it as a global tool with `uv`:
35
35
 
36
36
  ```bash
37
37
  # Install the readability tool
38
- uv tool install git+https://github.com/owahltinez/readability.git
38
+ uv tool install readability-cli
39
39
 
40
40
  # Use it anywhere
41
41
  readability check .
@@ -114,10 +114,19 @@ readability guide python --remote
114
114
  # Save a style guide to a file
115
115
  readability guide cpp --output cpp-style.md
116
116
 
117
+ # Print where the guide already lives, to search it without copying it
118
+ readability guide python --path
119
+ grep -n "Naming" "$(readability guide python --path)"
120
+
117
121
  # Synchronize all supported style guides to the local cache
118
122
  readability sync
119
123
  ```
120
124
 
125
+ A guide can exceed 100 KB, so `--path` is the cheap way to consult one: the
126
+ file is already on disk, and searching it beats reading it whole or writing
127
+ out a second copy. It is also what a coding agent should use, rather than
128
+ redirecting the guide into a repository.
129
+
121
130
  ### Supported Languages
122
131
 
123
132
  Use `readability languages` to see a full list of supported languages and
@@ -176,3 +185,17 @@ Check code style with `ruff`:
176
185
  uv run ruff check .
177
186
  uv run ruff format .
178
187
  ```
188
+
189
+ ### Releasing
190
+
191
+ Releases are published to PyPI as
192
+ [`readability-cli`](https://pypi.org/project/readability-cli/) via trusted
193
+ publishing: pushing a `v*` tag triggers the `publish.yml` GitHub Actions
194
+ workflow, which builds the package with `uv build` and uploads it.
195
+
196
+ ```bash
197
+ # 1. Bump the version in pyproject.toml, commit, and push
198
+ # 2. Tag the release and push the tag
199
+ git tag v0.4.1
200
+ git push origin v0.4.1
201
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "readability-cli"
3
- version = "0.4.0"
3
+ version = "0.6.0"
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"
@@ -253,6 +253,38 @@ def cli(ctx: click.Context, verbose: bool) -> None:
253
253
  logger.setLevel(logging.DEBUG)
254
254
 
255
255
 
256
+ def _echo_guide_path(language: str) -> None:
257
+ """Print the local path of a language's style guide.
258
+
259
+ Args:
260
+ language: The language whose guide is being located.
261
+
262
+ Raises:
263
+ SystemExit: If the language is unsupported or its guide is absent.
264
+ """
265
+ filename = LANGUAGE_MAP.get(language.lower())
266
+ if not filename:
267
+ click.echo(
268
+ f"Error: Language '{language}' is not supported. Supported "
269
+ f"languages: {', '.join(sorted(LANGUAGE_MAP.keys()))}",
270
+ err=True,
271
+ )
272
+ sys.exit(1)
273
+
274
+ local_path = get_local_path(filename)
275
+ if not os.path.exists(local_path):
276
+ # Naming a file that is not there would send the caller to read
277
+ # nothing; fetching it is what `sync` is for.
278
+ click.echo(
279
+ f"Error: No local guide for '{language}' at {local_path}. "
280
+ "Run 'readability sync' to fetch it.",
281
+ err=True,
282
+ )
283
+ sys.exit(1)
284
+
285
+ click.echo(local_path)
286
+
287
+
256
288
  @cli.command()
257
289
  @click.argument("language")
258
290
  @click.option(
@@ -264,9 +296,20 @@ def cli(ctx: click.Context, verbose: bool) -> None:
264
296
  @click.option(
265
297
  "--remote", "-r", is_flag=True, help="Force fetching from the web."
266
298
  )
299
+ @click.option(
300
+ "--path",
301
+ "-p",
302
+ "show_path",
303
+ is_flag=True,
304
+ help="Print where the guide is stored instead of its contents.",
305
+ )
267
306
  @click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging.")
268
307
  def guide(
269
- language: str, output: Optional[str], remote: bool, verbose: bool
308
+ language: str,
309
+ output: Optional[str],
310
+ remote: bool,
311
+ show_path: bool,
312
+ verbose: bool,
270
313
  ) -> None:
271
314
  """Fetch the style guide for a specific LANGUAGE."""
272
315
  if verbose:
@@ -274,6 +317,13 @@ def guide(
274
317
 
275
318
  logger.info("Processing style guide for: %s", language)
276
319
 
320
+ # A guide runs to hundreds of kilobytes, so a caller that wants a few
321
+ # rules is better served searching the copy already on disk than
322
+ # reading the whole thing or writing out a second one.
323
+ if show_path:
324
+ _echo_guide_path(language)
325
+ return
326
+
277
327
  try:
278
328
  # Fetch and process the style guide
279
329
  markdown_content = get_guide(language, remote=remote)
@@ -362,32 +412,47 @@ def languages() -> None:
362
412
  )
363
413
  @click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging.")
364
414
  def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
365
- """Run relevant formatters and linters for given paths."""
415
+ """Run relevant formatters and linters for given paths.
416
+
417
+ Exits with a non-zero status code if any tool reports findings, so the
418
+ command can gate scripts and CI.
419
+ """
366
420
  if verbose:
367
421
  logger.setLevel(logging.DEBUG)
368
422
 
369
423
  # Resolve project root once for trigger file checking
370
424
  project_root = Path.cwd()
371
425
 
372
- # Process each provided path independently
426
+ # Process each provided path independently, tracking findings across
427
+ # all of them so the exit code reflects the overall result
428
+ found_issues = False
373
429
  for path_str in paths:
374
- _check_path(Path(path_str), project_root, fix=fix)
430
+ found_issues |= _check_path(Path(path_str), project_root, fix=fix)
375
431
 
432
+ if found_issues:
433
+ sys.exit(1)
376
434
 
377
- def _check_path(path: Path, project_root: Path, fix: bool = False) -> None:
435
+
436
+ def _check_path(path: Path, project_root: Path, fix: bool = False) -> bool:
378
437
  """Apply relevant tools to a single path.
379
438
 
380
439
  Args:
381
440
  path: The path (file or directory) to check.
382
441
  project_root: The root of the project for trigger file discovery.
383
442
  fix: Whether to apply automatic fixes.
443
+
444
+ Returns:
445
+ True if any tool reported findings, False otherwise.
384
446
  """
385
447
  logger.info("Checking path: %s", path)
386
448
 
387
449
  # Iterate through all supported tool definitions
450
+ found_issues = False
388
451
  for tool in _get_tool_definitions(path, project_root):
389
452
  if _should_run_tool(tool, path, project_root):
390
- _run_tool(tool["name"], tool, fix=fix)
453
+ found_issues |= _run_tool(tool["name"], tool, fix=fix)
454
+
455
+ return found_issues
391
456
 
392
457
 
393
458
  def _should_run_tool(
@@ -642,13 +707,16 @@ def _run_tool(
642
707
  tool_name: str,
643
708
  tool_config: dict[str, Any],
644
709
  fix: bool = False,
645
- ) -> None:
710
+ ) -> bool:
646
711
  """Orchestrate the execution of a specific formatting or linting tool.
647
712
 
648
713
  Args:
649
714
  tool_name: The name of the tool to run.
650
715
  tool_config: The tool configuration dictionary.
651
716
  fix: Whether to apply automatic fixes.
717
+
718
+ Returns:
719
+ True if the tool reported findings, False otherwise.
652
720
  """
653
721
  # Identify the primary command to check for executable availability
654
722
  cmd = (
@@ -658,16 +726,17 @@ def _run_tool(
658
726
  or tool_config.get("check_format")
659
727
  )
660
728
  if not cmd:
661
- return
729
+ return False
662
730
 
663
731
  executable = str(cmd[0])
664
732
  if not shutil.which(executable):
665
733
  logger.debug(
666
734
  "Tool %s (%s) not found in PATH, skipping.", tool_name, executable
667
735
  )
668
- return
736
+ return False
669
737
 
670
738
  logger.info("Running %s...", tool_name)
739
+ found_issues = False
671
740
  try:
672
741
  if fix:
673
742
  # 1. Run formatters (if available) - these are expected to
@@ -691,6 +760,7 @@ def _run_tool(
691
760
  if result.returncode != 0 or (
692
761
  tool_name == "go fmt" and result.stdout.strip()
693
762
  ):
763
+ found_issues = True
694
764
  click.echo(
695
765
  f"--- {tool_name} formatting findings ---\n"
696
766
  f"{result.stdout}\n{result.stderr}"
@@ -707,6 +777,7 @@ def _run_tool(
707
777
  timeout=DEFAULT_TIMEOUT,
708
778
  )
709
779
  if result.returncode != 0:
780
+ found_issues = True
710
781
  click.echo(
711
782
  f"--- {tool_name} findings ---\n"
712
783
  f"{result.stdout}\n{result.stderr}"
@@ -721,6 +792,8 @@ def _run_tool(
721
792
  except (subprocess.SubprocessError, OSError) as e:
722
793
  logger.warning("Unexpected error while running %s: %s", tool_name, e)
723
794
 
795
+ return found_issues
796
+
724
797
 
725
798
  def _execute_tool_command(cmd: list[str]) -> None:
726
799
  """Execute a tool command, raising if it exits with a non-zero code.
@@ -151,6 +151,52 @@ def test_cli_output_file(mock_guide: MagicMock, tmp_path: Path) -> None:
151
151
  assert output_file.read_text() == "# Style Guide"
152
152
 
153
153
 
154
+ def test_cli_path_prints_the_location(tmp_path: Path, monkeypatch) -> None:
155
+ """Tests that --path reports where the guide already lives on disk.
156
+
157
+ Guides ship with the package, so a caller wanting to search one needs
158
+ its location, not another copy of its contents.
159
+ """
160
+ monkeypatch.setenv("READABILITY_CACHE", str(tmp_path))
161
+ (tmp_path / "pyguide.md").write_text("# Style Guide")
162
+
163
+ runner = CliRunner()
164
+ result = runner.invoke(cli, ["guide", "python", "--path"])
165
+
166
+ assert result.exit_code == 0
167
+ assert result.output.strip() == str(tmp_path / "pyguide.md")
168
+ assert "# Style Guide" not in result.output
169
+
170
+
171
+ def test_cli_path_reports_a_guide_not_yet_fetched(
172
+ tmp_path: Path, monkeypatch
173
+ ) -> None:
174
+ """Tests that a missing local guide fails instead of naming a phantom.
175
+
176
+ Printing a path to a file that is not there would send the caller to
177
+ read nothing at all.
178
+ """
179
+ monkeypatch.setenv("READABILITY_CACHE", str(tmp_path))
180
+
181
+ runner = CliRunner()
182
+ result = runner.invoke(cli, ["guide", "python", "--path"])
183
+
184
+ assert result.exit_code == 1
185
+ assert "sync" in result.output
186
+
187
+
188
+ def test_cli_path_rejects_an_unsupported_language(
189
+ tmp_path: Path, monkeypatch
190
+ ) -> None:
191
+ """Tests that --path validates the language like the default path does."""
192
+ monkeypatch.setenv("READABILITY_CACHE", str(tmp_path))
193
+
194
+ runner = CliRunner()
195
+ result = runner.invoke(cli, ["guide", "nonexistent", "--path"])
196
+
197
+ assert result.exit_code == 1
198
+
199
+
154
200
  @patch("readability.get_guide")
155
201
  def test_cli_verbose(mock_guide: MagicMock) -> None:
156
202
  """Tests CLI with verbose flag.
@@ -559,3 +605,107 @@ def test_bundled_default_configs_are_valid(tmp_path: Path) -> None:
559
605
  ruff_config = tomllib.loads(_bundled_config("ruff").read_text())
560
606
  assert ruff_config["line-length"] == 80
561
607
  assert ruff_config["lint"]["pydocstyle"]["convention"] == "google"
608
+
609
+
610
+ @patch("shutil.which")
611
+ @patch("subprocess.run")
612
+ def test_check_command_exits_nonzero_on_format_findings(
613
+ mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
614
+ ) -> None:
615
+ """Tests that formatting findings produce a non-zero exit code.
616
+
617
+ Args:
618
+ mock_run: The mocked subprocess.run function.
619
+ mock_which: The mocked shutil.which function.
620
+ tmp_path: The temporary directory fixture.
621
+ """
622
+ mock_which.side_effect = lambda x: x if x == "ruff" else None
623
+
624
+ # Only `ruff format --check` reports findings
625
+ def run_side_effect(cmd, **kwargs):
626
+ if "format" in cmd:
627
+ return MagicMock(
628
+ returncode=1, stdout="Would reformat: script.py", stderr=""
629
+ )
630
+ return MagicMock(returncode=0, stdout="", stderr="")
631
+
632
+ mock_run.side_effect = run_side_effect
633
+
634
+ runner = CliRunner()
635
+ with runner.isolated_filesystem(temp_dir=tmp_path):
636
+ Path("pyproject.toml").touch()
637
+ Path("script.py").touch()
638
+
639
+ result = runner.invoke(cli, ["check", "script.py"])
640
+
641
+ assert "formatting findings" in result.output
642
+ assert result.exit_code == 1
643
+
644
+
645
+ @patch("shutil.which")
646
+ @patch("subprocess.run")
647
+ def test_check_command_exits_nonzero_on_check_findings(
648
+ mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
649
+ ) -> None:
650
+ """Tests that linter findings produce a non-zero exit code.
651
+
652
+ Args:
653
+ mock_run: The mocked subprocess.run function.
654
+ mock_which: The mocked shutil.which function.
655
+ tmp_path: The temporary directory fixture.
656
+ """
657
+ mock_which.side_effect = lambda x: x if x == "ruff" else None
658
+
659
+ # Only `ruff check` reports findings
660
+ def run_side_effect(cmd, **kwargs):
661
+ if "check" in cmd and "format" not in cmd:
662
+ return MagicMock(
663
+ returncode=1, stdout="E501 line too long", stderr=""
664
+ )
665
+ return MagicMock(returncode=0, stdout="", stderr="")
666
+
667
+ mock_run.side_effect = run_side_effect
668
+
669
+ runner = CliRunner()
670
+ with runner.isolated_filesystem(temp_dir=tmp_path):
671
+ Path("pyproject.toml").touch()
672
+ Path("script.py").touch()
673
+
674
+ result = runner.invoke(cli, ["check", "script.py"])
675
+
676
+ assert "findings" in result.output
677
+ assert result.exit_code == 1
678
+
679
+
680
+ @patch("shutil.which")
681
+ @patch("subprocess.run")
682
+ def test_check_command_fix_exits_nonzero_on_remaining_findings(
683
+ mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
684
+ ) -> None:
685
+ """Tests that findings remaining after --fix produce a non-zero exit.
686
+
687
+ Args:
688
+ mock_run: The mocked subprocess.run function.
689
+ mock_which: The mocked shutil.which function.
690
+ tmp_path: The temporary directory fixture.
691
+ """
692
+ mock_which.side_effect = lambda x: x if x == "ruff" else None
693
+
694
+ # Fixers succeed, but the check step still reports findings
695
+ def run_side_effect(cmd, **kwargs):
696
+ if "check" in cmd and "--fix" not in cmd:
697
+ return MagicMock(
698
+ returncode=1, stdout="E501 line too long", stderr=""
699
+ )
700
+ return MagicMock(returncode=0, stdout="", stderr="")
701
+
702
+ mock_run.side_effect = run_side_effect
703
+
704
+ runner = CliRunner()
705
+ with runner.isolated_filesystem(temp_dir=tmp_path):
706
+ Path("pyproject.toml").touch()
707
+ Path("script.py").touch()
708
+
709
+ result = runner.invoke(cli, ["check", "--fix", "script.py"])
710
+
711
+ assert result.exit_code == 1
@@ -194,7 +194,7 @@ wheels = [
194
194
 
195
195
  [[package]]
196
196
  name = "readability-cli"
197
- version = "0.4.0"
197
+ version = "0.6.0"
198
198
  source = { editable = "." }
199
199
  dependencies = [
200
200
  { name = "beautifulsoup4" },
File without changes