Coverage for src / rhiza_tools / cli.py: 88%
76 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-23 01:10 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-23 01:10 +0000
1"""CLI commands for Rhiza Tools.
3This module defines the main Typer application and all command-line interface
4commands for rhiza-tools. It provides commands for version bumping, coverage
5badge generation, release management, and README updates.
7The CLI can be used either as a standalone tool (`rhiza-tools`) or as a
8subcommand of the rhiza CLI (`rhiza tools`).
10Example:
11 Bump version to a specific version::
13 $ rhiza-tools bump 1.2.3
14 $ rhiza tools bump 1.2.3
16 Bump version interactively::
18 $ rhiza-tools bump
20 Generate a coverage badge::
22 $ rhiza-tools generate-coverage-badge --coverage-json _tests/coverage.json
24 Update README with make help output::
26 $ rhiza-tools update-readme
27"""
29from pathlib import Path
30from typing import Annotated
32import typer
34from rhiza_tools import __version__, console
35from rhiza_tools.console import configure as configure_console
37from .commands.analyze_benchmarks import analyze_benchmarks_command
38from .commands.bump import bump_command
39from .commands.generate_badge import generate_coverage_badge_command
40from .commands.release import release_command
41from .commands.rollback import rollback_command
42from .commands.update_readme import update_readme_command
43from .commands.version_matrix import version_matrix_command
46def version_callback(value: bool) -> None:
47 """Display the version and exit."""
48 if value:
49 typer.echo(f"rhiza-tools version {__version__}")
50 raise typer.Exit()
53app = typer.Typer(help="Rhiza Tools - Extra utilities for Rhiza.")
55# Shared option so --verbose / -v works both before and after the subcommand.
56VERBOSE_OPTION = typer.Option(False, "--verbose", "-v", help="Show verbose debug output.")
59def _apply_verbose(verbose: bool) -> None:
60 """Enable verbose output if the flag was passed on the subcommand."""
61 if verbose:
62 configure_console(verbose=True)
65@app.callback()
66def main(
67 version: bool = typer.Option(
68 None,
69 "--version",
70 help="Show the version and exit.",
71 callback=version_callback,
72 is_eager=True,
73 ),
74 verbose: bool = VERBOSE_OPTION,
75) -> None:
76 """Rhiza Tools - Extra utilities for Rhiza."""
77 configure_console(verbose=verbose)
80@app.command()
81def bump(
82 version: str | None = typer.Argument(None, help="The version to bump to (e.g., 1.0.1, major, minor, patch, etc)"),
83 language: str | None = typer.Option(
84 None, "--language", "-l", help="Programming language (python or go). Auto-detected if not specified."
85 ),
86 dry_run: bool = typer.Option(False, "--dry-run", help="Print what would happen without doing it."),
87 commit: bool = typer.Option(False, "--commit", help="Commit the changes to git."),
88 push: bool = typer.Option(False, "--push", help="Push changes to remote after commit (implies --commit)."),
89 branch: str | None = typer.Option(
90 None, "--branch", help="Branch to perform the bump on (default: current branch)."
91 ),
92 allow_dirty: bool = typer.Option(
93 False, "--allow-dirty", help="Allow bumping even if the working directory is dirty."
94 ),
95 verbose: bool = VERBOSE_OPTION,
96) -> None:
97 """Bump the version of the project.
99 This command updates the version for Python (pyproject.toml) or Go (VERSION file)
100 projects using semantic versioning. You can provide an explicit version number,
101 a bump type (patch, minor, major), or leave it blank for an interactive prompt.
103 Args:
104 version: The version to bump to. Can be an explicit version (e.g., "1.2.3"),
105 a bump type ("patch", "minor", "major"), a prerelease type
106 ("alpha", "beta", "rc", "dev"), or None for interactive selection.
107 language: Programming language (python or go). Auto-detected if not specified.
108 dry_run: If True, show what would change without actually changing anything.
109 commit: If True, automatically commit the version change to git.
110 push: If True, push changes to remote after commit (implies --commit).
111 branch: Branch to perform the bump on (default: current branch).
112 allow_dirty: If True, allow bumping even with uncommitted changes.
113 verbose: If True, enable verbose debug output.
115 Example:
116 Bump to a specific version::
118 $ rhiza-tools bump 2.0.0
120 Bump patch version (1.2.3 -> 1.2.4)::
122 $ rhiza-tools bump patch
124 Bump a Go project explicitly::
126 $ rhiza-tools bump minor --language go
128 Preview changes without applying them::
130 $ rhiza-tools bump minor --dry-run
132 Interactive version selection::
134 $ rhiza-tools bump
136 Bump and push to remote::
138 $ rhiza-tools bump minor --push
139 """
140 _apply_verbose(verbose)
141 from rhiza_tools.commands.bump import BumpOptions, Language
143 # Parse language if provided
144 lang_enum = None
145 if language:
146 try:
147 lang_enum = Language(language.lower())
148 except ValueError:
149 console.error(f"Invalid language: {language}")
150 console.error("Supported languages: python, go")
151 raise typer.Exit(code=1) from None
153 options = BumpOptions(
154 version=version,
155 dry_run=dry_run,
156 commit=commit,
157 push=push,
158 branch=branch,
159 allow_dirty=allow_dirty,
160 language=lang_enum,
161 )
162 bump_command(options)
165@app.command()
166def generate_coverage_badge(
167 coverage_json: Annotated[
168 Path,
169 typer.Option(
170 "--coverage-json",
171 help="Path to coverage.json file",
172 ),
173 ] = Path("_tests/coverage.json"),
174 output: Annotated[
175 Path,
176 typer.Option(
177 help="Path to output badge JSON",
178 ),
179 ] = Path("_book/tests/coverage-badge.json"),
180 verbose: bool = VERBOSE_OPTION,
181) -> None:
182 """Generate a coverage badge for the project.
184 Reads a coverage report JSON file and creates a shields.io endpoint JSON file
185 for displaying a coverage badge. The badge color automatically adjusts based
186 on the coverage percentage.
188 Args:
189 coverage_json: Path to the coverage.json file generated by pytest-cov.
190 output: Path where the badge JSON file should be written.
191 verbose: If True, enable verbose debug output.
193 Example:
194 Generate badge with default paths::
196 $ rhiza-tools generate-coverage-badge
198 Generate badge with custom paths::
200 $ rhiza-tools generate-coverage-badge \
201 --coverage-json tests/coverage.json \
202 --output assets/badge.json
203 """
204 _apply_verbose(verbose)
205 generate_coverage_badge_command(coverage_json_path=coverage_json, output_path=output)
208@app.command()
209def release(
210 bump: str | None = typer.Option(None, "--bump", help="Bump type (MAJOR, MINOR, PATCH) before release."),
211 with_bump: bool = typer.Option(
212 False,
213 "--with-bump",
214 help="Interactively select bump type before release (works with --dry-run).",
215 ),
216 push: bool = typer.Option(False, "--push", help="Push changes to remote (default: prompt in interactive mode)."),
217 dry_run: bool = typer.Option(False, "--dry-run", help="Print what would happen without doing it."),
218 non_interactive: bool = typer.Option(False, "--non-interactive", "-y", help="Skip all confirmation prompts."),
219 language: str | None = typer.Option(
220 None, "--language", "-l", help="Programming language (python or go). Auto-detected if not specified."
221 ),
222 verbose: bool = VERBOSE_OPTION,
223) -> None:
224 """Push a release tag to remote to trigger the release workflow.
226 This command validates the repository state and pushes the git tag for the
227 current version to the remote repository, which triggers the automated release
228 workflow. Supports Python projects (pyproject.toml) and Go projects
229 (go.mod + VERSION file). The project language is auto-detected when not
230 explicitly specified.
232 Args:
233 bump: Bump type (MAJOR, MINOR, PATCH) to apply before release.
234 with_bump: If True, interactively select bump type before release.
235 push: If True, push changes without prompting (implies non-interactive for push).
236 dry_run: If True, show what would happen without actually pushing the tag.
237 non_interactive: If True, skip all confirmation prompts (useful for CI/CD).
238 language: Programming language (python or go). Auto-detected if not specified.
239 verbose: If True, enable verbose debug output.
241 Example:
242 Push a release tag (with prompts)::
244 $ rhiza-tools release
246 Preview what would happen::
248 $ rhiza-tools release --dry-run
250 Non-interactive mode (for CI/CD)::
252 $ rhiza-tools release --non-interactive
254 Bump version and release::
256 $ rhiza-tools release --bump MINOR --push
258 Interactive bump with dry-run preview::
260 $ rhiza-tools release --with-bump --push --dry-run
262 Release a Go project::
264 $ rhiza-tools release --language go
265 """
266 _apply_verbose(verbose)
267 from rhiza_tools.commands.bump import Language
269 lang_enum = None
270 if language:
271 try:
272 lang_enum = Language(language.lower())
273 except ValueError:
274 console.error(f"Invalid language: {language}")
275 console.error("Supported languages: python, go")
276 raise typer.Exit(code=1) from None
278 release_command(bump, push, dry_run, non_interactive, with_bump, lang_enum)
281@app.command()
282def rollback(
283 tag: str | None = typer.Argument(None, help="Tag to rollback (e.g., v1.2.3). Interactive if omitted."),
284 revert_bump: bool = typer.Option(False, "--revert-bump", help="Also revert the version bump commit."),
285 dry_run: bool = typer.Option(False, "--dry-run", help="Print what would happen without doing it."),
286 non_interactive: bool = typer.Option(False, "--non-interactive", "-y", help="Skip all confirmation prompts."),
287 verbose: bool = VERBOSE_OPTION,
288) -> None:
289 """Rollback a release and/or version bump.
291 This command safely reverses release and bump operations by deleting
292 the release tag from local and remote repositories, and optionally
293 reverting the version bump commit.
295 It uses ``git revert`` rather than ``git reset``, making it safe
296 even when changes have already been pushed to remote.
298 Args:
299 tag: The tag to rollback (e.g., "v1.2.3"). If omitted, an interactive
300 menu shows recent tags to choose from.
301 revert_bump: If True, also revert the version bump commit associated
302 with the tag.
303 dry_run: If True, show what would happen without actually making changes.
304 non_interactive: If True, skip all confirmation prompts (useful for CI/CD).
305 verbose: If True, enable verbose debug output.
307 Example:
308 Rollback the most recent release interactively::
310 $ rhiza-tools rollback
312 Preview rollback of a specific tag::
314 $ rhiza-tools rollback v1.2.3 --dry-run
316 Fully rollback including the bump commit::
318 $ rhiza-tools rollback v1.2.3 --revert-bump
320 Non-interactive rollback (for CI/CD)::
322 $ rhiza-tools rollback v1.2.3 --revert-bump -y
323 """
324 _apply_verbose(verbose)
325 from rhiza_tools.commands.rollback import RollbackOptions
327 options = RollbackOptions(
328 tag=tag,
329 revert_bump=revert_bump,
330 dry_run=dry_run,
331 non_interactive=non_interactive,
332 )
333 rollback_command(options)
336@app.command(name="update-readme")
337def update_readme(
338 dry_run: bool = typer.Option(False, "--dry-run", help="Print what would happen without doing it."),
339 verbose: bool = VERBOSE_OPTION,
340) -> None:
341 """Update README.md with the current output from `make help`.
343 This command runs `make help` and updates the README.md file with the current
344 help output, keeping the documentation in sync with available Make targets.
346 Args:
347 dry_run: If True, show the help output that would be inserted without
348 actually modifying README.md.
349 verbose: If True, enable verbose debug output.
351 Example:
352 Update README with make help output::
354 $ rhiza-tools update-readme
356 Preview changes without modifying README::
358 $ rhiza-tools update-readme --dry-run
359 """
360 _apply_verbose(verbose)
361 update_readme_command(dry_run)
364@app.command(name="version-matrix")
365def version_matrix(
366 pyproject: Annotated[
367 Path,
368 typer.Option(
369 "--pyproject",
370 help="Path to pyproject.toml file",
371 ),
372 ] = Path("pyproject.toml"),
373 candidates: Annotated[
374 str | None,
375 typer.Option(
376 "--candidates",
377 help="Comma-separated list of candidate Python versions (e.g., '3.11,3.12,3.13')",
378 ),
379 ] = None,
380 verbose: bool = VERBOSE_OPTION,
381) -> None:
382 """Emit supported Python versions from pyproject.toml as JSON.
384 This command reads the requires-python field from pyproject.toml and outputs
385 a JSON array of Python versions that satisfy the constraint. This is primarily
386 used in GitHub Actions to compute the test matrix.
388 Args:
389 pyproject: Path to the pyproject.toml file.
390 candidates: Comma-separated list of candidate Python versions to evaluate.
391 Defaults to "3.11,3.12,3.13,3.14".
392 verbose: If True, enable verbose debug output.
394 Example:
395 Get supported versions with defaults::
397 $ rhiza-tools version-matrix
398 ["3.11", "3.12"]
400 Use custom pyproject.toml path::
402 $ rhiza-tools version-matrix --pyproject /path/to/pyproject.toml
404 Use custom candidates::
406 $ rhiza-tools version-matrix --candidates "3.10,3.11,3.12"
407 """
408 _apply_verbose(verbose)
409 candidates_list = None
410 if candidates:
411 candidates_list = [v.strip() for v in candidates.split(",")]
413 version_matrix_command(pyproject_path=pyproject, candidates=candidates_list)
416@app.command(name="analyze-benchmarks")
417def analyze_benchmarks(
418 benchmarks_json: Annotated[
419 Path,
420 typer.Option(
421 "--benchmarks-json",
422 help="Path to benchmarks.json file",
423 ),
424 ] = Path("_benchmarks/benchmarks.json"),
425 output_html: Annotated[
426 Path,
427 typer.Option(
428 "--output-html",
429 help="Path to save HTML visualization",
430 ),
431 ] = Path("_benchmarks/benchmarks.html"),
432 verbose: bool = VERBOSE_OPTION,
433) -> None:
434 """Analyze pytest-benchmark results and visualize them.
436 This command reads a benchmarks.json file produced by pytest-benchmark,
437 prints a table with benchmark name, mean milliseconds, and operations per
438 second, and generates an interactive Plotly bar chart of mean runtimes.
440 Note: This command requires pandas and plotly. Install with:
441 uv pip install -e '.[dev]' or pip install 'rhiza-tools[dev]'
443 Args:
444 benchmarks_json: Path to the benchmarks.json file.
445 output_html: Path where the HTML visualization should be saved.
446 verbose: If True, enable verbose debug output.
448 Example:
449 Analyze benchmarks with default paths::
451 $ rhiza-tools analyze-benchmarks
453 Use custom paths::
455 $ rhiza-tools analyze-benchmarks \
456 --benchmarks-json tests/benchmarks.json \
457 --output-html reports/benchmarks.html
458 """
459 _apply_verbose(verbose)
460 analyze_benchmarks_command(benchmarks_json=benchmarks_json, output_html=output_html)