readability-cli 0.9.0__tar.gz → 0.10.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 (38) hide show
  1. {readability_cli-0.9.0 → readability_cli-0.10.0}/PKG-INFO +35 -3
  2. {readability_cli-0.9.0 → readability_cli-0.10.0}/README.md +34 -2
  3. {readability_cli-0.9.0 → readability_cli-0.10.0}/pyproject.toml +1 -1
  4. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/checking.py +35 -20
  5. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/cli.py +8 -21
  6. readability_cli-0.10.0/readability/tools.py +560 -0
  7. {readability_cli-0.9.0 → readability_cli-0.10.0}/test_checking.py +655 -37
  8. readability_cli-0.9.0/readability/tools.py +0 -407
  9. {readability_cli-0.9.0 → readability_cli-0.10.0}/.github/workflows/ci.yml +0 -0
  10. {readability_cli-0.9.0 → readability_cli-0.10.0}/.github/workflows/publish.yml +0 -0
  11. {readability_cli-0.9.0 → readability_cli-0.10.0}/.github/workflows/update-guides.yml +0 -0
  12. {readability_cli-0.9.0 → readability_cli-0.10.0}/.gitignore +0 -0
  13. {readability_cli-0.9.0 → readability_cli-0.10.0}/.python-version +0 -0
  14. {readability_cli-0.9.0 → readability_cli-0.10.0}/LICENSE +0 -0
  15. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/__init__.py +0 -0
  16. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/configs/biome-default.json +0 -0
  17. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/configs/pyrefly.toml +0 -0
  18. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/configs/ruff.toml +0 -0
  19. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/guide.py +0 -0
  20. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/guides/Rguide.md +0 -0
  21. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/guides/cppguide.md +0 -0
  22. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/guides/csharp-style.md +0 -0
  23. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/guides/docguide-style.md +0 -0
  24. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/guides/go-guide.md +0 -0
  25. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/guides/htmlcssguide.md +0 -0
  26. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/guides/javaguide.md +0 -0
  27. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/guides/jsguide.md +0 -0
  28. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/guides/jsoncstyleguide.md +0 -0
  29. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/guides/objcguide.md +0 -0
  30. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/guides/pyguide.md +0 -0
  31. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/guides/shellguide.md +0 -0
  32. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/guides/tsguide.md +0 -0
  33. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/guides/vimscriptguide.md +0 -0
  34. {readability_cli-0.9.0 → readability_cli-0.10.0}/readability/outline.py +0 -0
  35. {readability_cli-0.9.0 → readability_cli-0.10.0}/test_guide.py +0 -0
  36. {readability_cli-0.9.0 → readability_cli-0.10.0}/test_outline.py +0 -0
  37. {readability_cli-0.9.0 → readability_cli-0.10.0}/test_package.py +0 -0
  38. {readability_cli-0.9.0 → readability_cli-0.10.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: readability-cli
3
- Version: 0.9.0
3
+ Version: 0.10.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
@@ -168,8 +168,13 @@ The returned `CheckReport` records only whether findings occurred and which
168
168
  tools ran, were skipped, or failed. Detailed tool findings are still written as
169
169
  each tool runs. Every path is validated before any tool runs, and a missing one
170
170
  raises `FileNotFoundError` rather than being misreported as a finding. Relative
171
- paths remain relative to the process working directory; `project_root` controls
172
- configuration discovery only.
171
+ paths remain relative to the process working directory. `project_root` bounds
172
+ configuration discovery, locates project-local tool installs, and roots
173
+ ignore-file discovery. Because it bounds discovery, a caller that passes one
174
+ gets the same verdict for the same files wherever they sit, which is what makes
175
+ the result usable as a baseline. It defaults to the repository the process is
176
+ in, so the command agrees with the tools rather than with the directory the
177
+ caller happened to be standing in.
173
178
 
174
179
  ### Configuring Formats
175
180
 
@@ -189,6 +194,33 @@ into it through that tool's native configuration. Gofmt has no project settings.
189
194
  The bundled Biome file requires Biome 2.5 or later, matching the fallback
190
195
  runner's version floor.
191
196
 
197
+ Configuration is found per file, in the file's own directory and then its
198
+ ancestors, which is where each canonical tool looks. A package keeping its
199
+ settings in a subdirectory is checked against them however it is reached:
200
+
201
+ ```bash
202
+ # Uses web/biome.json, not the bundled default
203
+ readability check web/app.ts
204
+
205
+ # Uses the repository root's [tool.ruff], not the bundled default
206
+ cd pkg && readability check module.py
207
+
208
+ # pkg/ is checked against pkg/pyproject.toml, the rest against the defaults
209
+ readability check .
210
+ ```
211
+
212
+ A tool is only told which configuration to use when the file has none, because
213
+ each one resolves its own hierarchy: a single Ruff invocation over two packages
214
+ declaring different line lengths reports each against its own. So a mixed tree
215
+ needs no choosing between configurations, and the bundled default never
216
+ overrides one the project declared.
217
+
218
+ Discovery is per tool, so configuring one leaves the others on their bundled
219
+ defaults. It is also bounded, by the repository the command runs in, or by
220
+ `project_root` when calling `check_paths` directly. Configuration outside that
221
+ boundary is not read, so the same paths get the same answer wherever the
222
+ repository sits.
223
+
192
224
  ## Style Guides
193
225
 
194
226
  The `guide` command reads a Google style guide, using the local cache when
@@ -154,8 +154,13 @@ The returned `CheckReport` records only whether findings occurred and which
154
154
  tools ran, were skipped, or failed. Detailed tool findings are still written as
155
155
  each tool runs. Every path is validated before any tool runs, and a missing one
156
156
  raises `FileNotFoundError` rather than being misreported as a finding. Relative
157
- paths remain relative to the process working directory; `project_root` controls
158
- configuration discovery only.
157
+ paths remain relative to the process working directory. `project_root` bounds
158
+ configuration discovery, locates project-local tool installs, and roots
159
+ ignore-file discovery. Because it bounds discovery, a caller that passes one
160
+ gets the same verdict for the same files wherever they sit, which is what makes
161
+ the result usable as a baseline. It defaults to the repository the process is
162
+ in, so the command agrees with the tools rather than with the directory the
163
+ caller happened to be standing in.
159
164
 
160
165
  ### Configuring Formats
161
166
 
@@ -175,6 +180,33 @@ into it through that tool's native configuration. Gofmt has no project settings.
175
180
  The bundled Biome file requires Biome 2.5 or later, matching the fallback
176
181
  runner's version floor.
177
182
 
183
+ Configuration is found per file, in the file's own directory and then its
184
+ ancestors, which is where each canonical tool looks. A package keeping its
185
+ settings in a subdirectory is checked against them however it is reached:
186
+
187
+ ```bash
188
+ # Uses web/biome.json, not the bundled default
189
+ readability check web/app.ts
190
+
191
+ # Uses the repository root's [tool.ruff], not the bundled default
192
+ cd pkg && readability check module.py
193
+
194
+ # pkg/ is checked against pkg/pyproject.toml, the rest against the defaults
195
+ readability check .
196
+ ```
197
+
198
+ A tool is only told which configuration to use when the file has none, because
199
+ each one resolves its own hierarchy: a single Ruff invocation over two packages
200
+ declaring different line lengths reports each against its own. So a mixed tree
201
+ needs no choosing between configurations, and the bundled default never
202
+ overrides one the project declared.
203
+
204
+ Discovery is per tool, so configuring one leaves the others on their bundled
205
+ defaults. It is also bounded, by the repository the command runs in, or by
206
+ `project_root` when calling `check_paths` directly. Configuration outside that
207
+ boundary is not read, so the same paths get the same answer wherever the
208
+ repository sits.
209
+
178
210
  ## Style Guides
179
211
 
180
212
  The `guide` command reads a Google style guide, using the local cache when
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "readability-cli"
3
- version = "0.9.0"
3
+ version = "0.10.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"
@@ -12,6 +12,7 @@ from readability.tools import (
12
12
  ToolPlan,
13
13
  _command_batches,
14
14
  _get_tool_definitions,
15
+ _repository_root,
15
16
  _should_run_tool,
16
17
  _tool_is_installed,
17
18
  )
@@ -74,8 +75,10 @@ def check_paths(
74
75
  Args:
75
76
  paths: Files or directories to check, as strings or paths. Relative
76
77
  paths are interpreted from the current working directory.
77
- project_root: Root used only to discover tool configuration. Defaults
78
- to the current working directory; it does not rebase paths.
78
+ project_root: Bounds config discovery, so the same files get the
79
+ same verdict wherever the tree sits, and locates tool installs
80
+ and ignore files. Defaults to the repository the process is in.
81
+ It does not rebase paths.
79
82
  fix: Whether to apply automatic fixes.
80
83
 
81
84
  Returns:
@@ -85,7 +88,11 @@ def check_paths(
85
88
  FileNotFoundError: If any requested path does not exist. Every path is
86
89
  validated before any tools run.
87
90
  """
88
- root = Path.cwd() if project_root is None else project_root
91
+ here = Path.cwd()
92
+ repository = _repository_root(here)
93
+ root = project_root if project_root is not None else (repository or here)
94
+ # A named root bounds the search; otherwise each path's repository does
95
+ boundary = project_root
89
96
  requested_paths = [Path(path) for path in paths]
90
97
  missing_path = next(
91
98
  (path for path in requested_paths if not path.exists()), None
@@ -95,7 +102,7 @@ def check_paths(
95
102
 
96
103
  report = CheckReport()
97
104
  for path in requested_paths:
98
- path_report = _check_path(path, root, fix=fix)
105
+ path_report = _check_path(path, root, boundary, fix=fix)
99
106
  if not path_report.ran:
100
107
  path_report.unverified_paths.append(path)
101
108
  report.absorb(path_report)
@@ -103,13 +110,18 @@ def check_paths(
103
110
 
104
111
 
105
112
  def _check_path(
106
- path: Path, project_root: Path, fix: bool = False
113
+ path: Path,
114
+ project_root: Path,
115
+ boundary: Path | None = None,
116
+ fix: bool = False,
107
117
  ) -> CheckReport:
108
118
  """Apply relevant tools to a single path.
109
119
 
110
120
  Args:
111
121
  path: The path (file or directory) to check.
112
- project_root: The root used for native tool configuration discovery.
122
+ project_root: Locates tool installs and ignore files.
123
+ boundary: Outermost directory config discovery may consider, or
124
+ None to bound it by the path's own repository.
113
125
  fix: Whether to apply automatic fixes.
114
126
 
115
127
  Returns:
@@ -117,14 +129,16 @@ def _check_path(
117
129
  """
118
130
  logger.info("Checking path: %s", path)
119
131
 
132
+ if boundary is None:
133
+ boundary = _repository_root(path) or Path(path.resolve().anchor)
134
+
120
135
  # Iterate through all supported tool definitions
121
136
  report = CheckReport()
122
- for tool in _get_tool_definitions(path, project_root):
137
+ for tool in _get_tool_definitions(path, project_root, boundary):
123
138
  if not _should_run_tool(tool, path):
124
139
  continue
125
140
 
126
- # A tool that is wanted but absent leaves a hole in the coverage,
127
- # which is not the same as a clean result
141
+ # A wanted but absent tool is a hole in coverage, not a clean result
128
142
  if not _tool_is_installed(tool):
129
143
  report.skipped.add(tool.name)
130
144
  continue
@@ -156,10 +170,7 @@ def _run_tool(
156
170
  target_count = len(tool.targets or ())
157
171
  try:
158
172
  if fix:
159
- # Formatters rewrite files, fixers apply what they can. Both
160
- # exit non-zero when something is left over, which is a finding
161
- # rather than a failure, so the check below still gets to run
162
- # and report what they could not deal with.
173
+ # Leftovers exit non-zero, which is a finding rather than a failure
163
174
  for phase in ("format", "fix"):
164
175
  configured_command = getattr(tool, phase)
165
176
  if configured_command:
@@ -208,8 +219,7 @@ def _run_tool(
208
219
  f"{result.stdout}\n{result.stderr}"
209
220
  )
210
221
 
211
- # Failing to start, or running past the timeout, means this tool checked
212
- # nothing. Whatever it managed before that stays in ran.
222
+ # Never starting, or timing out, means this tool checked nothing
213
223
  except (subprocess.SubprocessError, OSError) as e:
214
224
  logger.warning("Could not run %s: %s", tool.name, e)
215
225
  report.failed.add(tool.name)
@@ -227,14 +237,19 @@ def _tool_checked_files(
227
237
  result: The completed subprocess.
228
238
 
229
239
  Returns:
230
- False when Biome explicitly reports that it checked zero files.
240
+ False when the tool explicitly reports that it processed no files.
231
241
  """
232
- if tool_name != "biome":
242
+ # Biome says this for an unmatched target, Ruff when excludes cover them all
243
+ zero_file_reports = {
244
+ "biome": ("Checked 0 files", "Formatted 0 files"),
245
+ "ruff": ("No Python files found",),
246
+ }
247
+ summaries = zero_file_reports.get(tool_name)
248
+ if summaries is None:
233
249
  return True
250
+
234
251
  output = f"{result.stdout or ''}\n{result.stderr or ''}"
235
- # Biome 2.x uses these summaries for an unmatched target.
236
- zero_file_summaries = ("Checked 0 files", "Formatted 0 files")
237
- return not any(summary in output for summary in zero_file_summaries)
252
+ return not any(summary in output for summary in summaries)
238
253
 
239
254
 
240
255
  def _capture_tool_command(
@@ -60,8 +60,7 @@ def guide(
60
60
  _echo_languages()
61
61
  return
62
62
 
63
- # Refusing beats picking a winner: a silent precedence rule is how the
64
- # caller ends up reading the wrong thing without being told.
63
+ # Refusing beats a silent precedence rule the caller cannot see
65
64
  if full and reference:
66
65
  raise click.UsageError("--full takes the whole guide, so REF cannot.")
67
66
 
@@ -106,8 +105,7 @@ def _resolve_filenames(languages: Sequence[str]) -> list[str]:
106
105
  f"Language '{language}' is not supported. Supported "
107
106
  f"languages: {', '.join(sorted(LANGUAGE_MAP.keys()))}"
108
107
  )
109
- # Aliases such as 'cpp' and 'c++' resolve to one guide, which is
110
- # fetched once however many of its names were given
108
+ # Aliases share a guide, fetched once however many names were given
111
109
  if filename not in filenames:
112
110
  filenames.append(filename)
113
111
 
@@ -190,8 +188,7 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
190
188
  err=True,
191
189
  )
192
190
 
193
- # A tool that could not start or outlived the timeout checked nothing,
194
- # so its silence is not a pass however far the rest of the run got
191
+ # A tool that never ran checked nothing, so its silence is not a pass
195
192
  if report.failed:
196
193
  click.echo(
197
194
  f"Error: Could not run: {', '.join(sorted(report.failed))}. "
@@ -200,14 +197,11 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
200
197
  )
201
198
  sys.exit(1)
202
199
 
203
- # Findings remain a failed check even when a tool reports that it could
204
- # not process any files (for example, a Biome configuration error).
200
+ # Findings fail the check even if the tool processed no files
205
201
  if report.findings:
206
202
  sys.exit(1)
207
203
 
208
- # Having run nothing is not a pass. Reporting it as one is how this
209
- # command became a silent no-op wherever its tools were absent, gating
210
- # nothing while every caller read the exit code as approval.
204
+ # Having run nothing is not a pass, it is a silent no-op gating nothing
211
205
  if not report.ran and report.skipped:
212
206
  click.echo(
213
207
  f"Error: Every tool for {len(paths)} path(s) is missing, so "
@@ -216,9 +210,7 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
216
210
  )
217
211
  sys.exit(1)
218
212
 
219
- # No tool applying is a fact about the project rather than a fault: there
220
- # is nothing to install and nothing to fix. It still cannot be reported
221
- # as a clean result, because nothing was inspected.
213
+ # No tool applying is a fact, not a fault, but still not a clean result
222
214
  if not report.ran:
223
215
  click.echo(
224
216
  f"No tool applies to {len(paths)} path(s); nothing was checked.",
@@ -235,8 +227,7 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
235
227
  err=True,
236
228
  )
237
229
 
238
- # Findings are the only thing this command printed, so a clean run said
239
- # nothing at all and left the caller unable to tell it from a no-op.
230
+ # Without this a clean run printed nothing and read as a no-op
240
231
  click.echo(
241
232
  f"No findings in {checked_path_count} path(s) "
242
233
  f"({', '.join(sorted(report.ran))}).",
@@ -247,11 +238,7 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
247
238
  # Main entry point for the CLI
248
239
  def main() -> None:
249
240
  """Main entry point for the CLI."""
250
- # Configure logging here rather than at import time so that importing this
251
- # module as a library (e.g. from lemming) has no side effects
252
- # WARNING, not INFO: everything logged below it narrates progress, which
253
- # is what --verbose is for. Leaving it on meant there was no quiet mode
254
- # and the flag could only add DEBUG on top.
241
+ # Configured here, not at import, so library use has no side effects
255
242
  logging.basicConfig(
256
243
  level=logging.WARNING,
257
244
  format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",