Coverage for src / rhiza_tools / cli.py: 100%
77 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 02:21 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 02:21 +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.")
57CONFIG_OPTION = typer.Option(
58 None,
59 "--config",
60 "-c",
61 help="Path to the .cfg.toml bumpversion config file. Defaults to .rhiza/.cfg.toml.",
62)
65def _apply_verbose(verbose: bool) -> None:
66 """Enable verbose output if the flag was passed on the subcommand."""
67 if verbose:
68 configure_console(verbose=True)
71@app.callback()
72def main(
73 version: bool = typer.Option(
74 None,
75 "--version",
76 help="Show the version and exit.",
77 callback=version_callback,
78 is_eager=True,
79 ),
80 verbose: bool = VERBOSE_OPTION,
81) -> None:
82 """Rhiza Tools - Extra utilities for Rhiza."""
83 configure_console(verbose=verbose)
86@app.command()
87def bump(
88 version: str | None = typer.Argument(None, help="The version to bump to (e.g., 1.0.1, major, minor, patch, etc)"),
89 language: str | None = typer.Option(
90 None, "--language", "-l", help="Programming language (python or go). Auto-detected if not specified."
91 ),
92 dry_run: bool = typer.Option(False, "--dry-run", help="Print what would happen without doing it."),
93 commit: bool = typer.Option(False, "--commit", help="Commit the changes to git."),
94 push: bool = typer.Option(False, "--push", help="Push changes to remote after commit (implies --commit)."),
95 branch: str | None = typer.Option(
96 None, "--branch", help="Branch to perform the bump on (default: current branch)."
97 ),
98 allow_dirty: bool = typer.Option(
99 False, "--allow-dirty", help="Allow bumping even if the working directory is dirty."
100 ),
101 config: Path | None = CONFIG_OPTION,
102 verbose: bool = VERBOSE_OPTION,
103) -> None:
104 """Bump the version of the project.
106 This command updates the version for Python (pyproject.toml) or Go (VERSION file)
107 projects using semantic versioning. You can provide an explicit version number,
108 a bump type (patch, minor, major), or leave it blank for an interactive prompt.
110 Args:
111 version: The version to bump to. Can be an explicit version (e.g., "1.2.3"),
112 a bump type ("patch", "minor", "major"), a prerelease type
113 ("alpha", "beta", "rc", "dev"), or None for interactive selection.
114 language: Programming language (python or go). Auto-detected if not specified.
115 dry_run: If True, show what would change without actually changing anything.
116 commit: If True, automatically commit the version change to git.
117 push: If True, push changes to remote after commit (implies --commit).
118 branch: Branch to perform the bump on (default: current branch).
119 allow_dirty: If True, allow bumping even with uncommitted changes.
120 config: Path to the .cfg.toml bumpversion config file. Defaults to .rhiza/.cfg.toml.
121 verbose: If True, enable verbose debug output.
123 Example:
124 Bump to a specific version::
126 $ rhiza-tools bump 2.0.0
128 Bump patch version (1.2.3 -> 1.2.4)::
130 $ rhiza-tools bump patch
132 Bump a Go project explicitly::
134 $ rhiza-tools bump minor --language go
136 Preview changes without applying them::
138 $ rhiza-tools bump minor --dry-run
140 Interactive version selection::
142 $ rhiza-tools bump
144 Bump and push to remote::
146 $ rhiza-tools bump minor --push
148 Use a custom config file::
150 $ rhiza-tools bump patch --config /path/to/my.cfg.toml
151 """
152 _apply_verbose(verbose)
153 from rhiza_tools.commands.bump import BumpOptions, Language
155 # Parse language if provided
156 lang_enum = None
157 if language:
158 try:
159 lang_enum = Language(language.lower())
160 except ValueError:
161 console.error(f"Invalid language: {language}")
162 console.error("Supported languages: python, go")
163 raise typer.Exit(code=1) from None
165 options = BumpOptions(
166 version=version,
167 dry_run=dry_run,
168 commit=commit,
169 push=push,
170 branch=branch,
171 allow_dirty=allow_dirty,
172 language=lang_enum,
173 config=config,
174 )
175 bump_command(options)
178@app.command()
179def generate_coverage_badge(
180 coverage_json: Annotated[
181 Path,
182 typer.Option(
183 "--coverage-json",
184 help="Path to coverage.json file",
185 ),
186 ] = Path("_tests/coverage.json"),
187 output: Annotated[
188 Path,
189 typer.Option(
190 help="Path to output badge JSON",
191 ),
192 ] = Path("_book/tests/coverage-badge.json"),
193 verbose: bool = VERBOSE_OPTION,
194) -> None:
195 """Generate a coverage badge for the project.
197 Reads a coverage report JSON file and creates a shields.io endpoint JSON file
198 for displaying a coverage badge. The badge color automatically adjusts based
199 on the coverage percentage.
201 Args:
202 coverage_json: Path to the coverage.json file generated by pytest-cov.
203 output: Path where the badge JSON file should be written.
204 verbose: If True, enable verbose debug output.
206 Example:
207 Generate badge with default paths::
209 $ rhiza-tools generate-coverage-badge
211 Generate badge with custom paths::
213 $ rhiza-tools generate-coverage-badge \
214 --coverage-json tests/coverage.json \
215 --output assets/badge.json
216 """
217 _apply_verbose(verbose)
218 generate_coverage_badge_command(coverage_json_path=coverage_json, output_path=output)
221@app.command()
222def release(
223 bump: str | None = typer.Option(None, "--bump", help="Bump type (MAJOR, MINOR, PATCH) before release."),
224 with_bump: bool = typer.Option(
225 False,
226 "--with-bump",
227 help="Interactively select bump type before release (works with --dry-run).",
228 ),
229 push: bool = typer.Option(False, "--push", help="Push changes to remote (default: prompt in interactive mode)."),
230 dry_run: bool = typer.Option(False, "--dry-run", help="Print what would happen without doing it."),
231 non_interactive: bool = typer.Option(False, "--non-interactive", "-y", help="Skip all confirmation prompts."),
232 language: str | None = typer.Option(
233 None, "--language", "-l", help="Programming language (python or go). Auto-detected if not specified."
234 ),
235 config: Path | None = CONFIG_OPTION,
236 verbose: bool = VERBOSE_OPTION,
237) -> None:
238 """Push a release tag to remote to trigger the release workflow.
240 This command validates the repository state and pushes the git tag for the
241 current version to the remote repository, which triggers the automated release
242 workflow. Supports Python projects (pyproject.toml) and Go projects
243 (go.mod + VERSION file). The project language is auto-detected when not
244 explicitly specified.
246 Args:
247 bump: Bump type (MAJOR, MINOR, PATCH) to apply before release.
248 with_bump: If True, interactively select bump type before release.
249 push: If True, push changes without prompting (implies non-interactive for push).
250 dry_run: If True, show what would happen without actually pushing the tag.
251 non_interactive: If True, skip all confirmation prompts (useful for CI/CD).
252 language: Programming language (python or go). Auto-detected if not specified.
253 config: Path to the .cfg.toml bumpversion config file. Passed through to bump when --with-bump is used.
254 verbose: If True, enable verbose debug output.
256 Example:
257 Push a release tag (with prompts)::
259 $ rhiza-tools release
261 Preview what would happen::
263 $ rhiza-tools release --dry-run
265 Non-interactive mode (for CI/CD)::
267 $ rhiza-tools release --non-interactive
269 Bump version and release::
271 $ rhiza-tools release --bump MINOR --push
273 Bump version and release with custom config::
275 $ rhiza-tools release --bump MINOR --push --config /path/to/.cfg.toml
277 Interactive bump with dry-run preview::
279 $ rhiza-tools release --with-bump --push --dry-run
281 Release a Go project::
283 $ rhiza-tools release --language go
284 """
285 _apply_verbose(verbose)
286 from rhiza_tools.commands.bump import Language
288 lang_enum = None
289 if language:
290 try:
291 lang_enum = Language(language.lower())
292 except ValueError:
293 console.error(f"Invalid language: {language}")
294 console.error("Supported languages: python, go")
295 raise typer.Exit(code=1) from None
297 release_command(bump, push, dry_run, non_interactive, with_bump, lang_enum, config)
300@app.command()
301def rollback(
302 tag: str | None = typer.Argument(None, help="Tag to rollback (e.g., v1.2.3). Interactive if omitted."),
303 revert_bump: bool = typer.Option(False, "--revert-bump", help="Also revert the version bump commit."),
304 dry_run: bool = typer.Option(False, "--dry-run", help="Print what would happen without doing it."),
305 non_interactive: bool = typer.Option(False, "--non-interactive", "-y", help="Skip all confirmation prompts."),
306 verbose: bool = VERBOSE_OPTION,
307) -> None:
308 """Rollback a release and/or version bump.
310 This command safely reverses release and bump operations by deleting
311 the release tag from local and remote repositories, and optionally
312 reverting the version bump commit.
314 It uses ``git revert`` rather than ``git reset``, making it safe
315 even when changes have already been pushed to remote.
317 Args:
318 tag: The tag to rollback (e.g., "v1.2.3"). If omitted, an interactive
319 menu shows recent tags to choose from.
320 revert_bump: If True, also revert the version bump commit associated
321 with the tag.
322 dry_run: If True, show what would happen without actually making changes.
323 non_interactive: If True, skip all confirmation prompts (useful for CI/CD).
324 verbose: If True, enable verbose debug output.
326 Example:
327 Rollback the most recent release interactively::
329 $ rhiza-tools rollback
331 Preview rollback of a specific tag::
333 $ rhiza-tools rollback v1.2.3 --dry-run
335 Fully rollback including the bump commit::
337 $ rhiza-tools rollback v1.2.3 --revert-bump
339 Non-interactive rollback (for CI/CD)::
341 $ rhiza-tools rollback v1.2.3 --revert-bump -y
342 """
343 _apply_verbose(verbose)
344 from rhiza_tools.commands.rollback import RollbackOptions
346 options = RollbackOptions(
347 tag=tag,
348 revert_bump=revert_bump,
349 dry_run=dry_run,
350 non_interactive=non_interactive,
351 )
352 rollback_command(options)
355@app.command(name="update-readme")
356def update_readme(
357 dry_run: bool = typer.Option(False, "--dry-run", help="Print what would happen without doing it."),
358 verbose: bool = VERBOSE_OPTION,
359) -> None:
360 """Update README.md with the current output from `make help`.
362 This command runs `make help` and updates the README.md file with the current
363 help output, keeping the documentation in sync with available Make targets.
365 Args:
366 dry_run: If True, show the help output that would be inserted without
367 actually modifying README.md.
368 verbose: If True, enable verbose debug output.
370 Example:
371 Update README with make help output::
373 $ rhiza-tools update-readme
375 Preview changes without modifying README::
377 $ rhiza-tools update-readme --dry-run
378 """
379 _apply_verbose(verbose)
380 update_readme_command(dry_run)
383@app.command(name="version-matrix")
384def version_matrix(
385 pyproject: Annotated[
386 Path,
387 typer.Option(
388 "--pyproject",
389 help="Path to pyproject.toml file",
390 ),
391 ] = Path("pyproject.toml"),
392 candidates: Annotated[
393 str | None,
394 typer.Option(
395 "--candidates",
396 help="Comma-separated list of candidate Python versions (e.g., '3.11,3.12,3.13')",
397 ),
398 ] = None,
399 verbose: bool = VERBOSE_OPTION,
400) -> None:
401 """Emit supported Python versions from pyproject.toml as JSON.
403 This command reads the requires-python field from pyproject.toml and outputs
404 a JSON array of Python versions that satisfy the constraint. This is primarily
405 used in GitHub Actions to compute the test matrix.
407 Args:
408 pyproject: Path to the pyproject.toml file.
409 candidates: Comma-separated list of candidate Python versions to evaluate.
410 Defaults to "3.11,3.12,3.13,3.14".
411 verbose: If True, enable verbose debug output.
413 Example:
414 Get supported versions with defaults::
416 $ rhiza-tools version-matrix
417 ["3.11", "3.12"]
419 Use custom pyproject.toml path::
421 $ rhiza-tools version-matrix --pyproject /path/to/pyproject.toml
423 Use custom candidates::
425 $ rhiza-tools version-matrix --candidates "3.10,3.11,3.12"
426 """
427 _apply_verbose(verbose)
428 candidates_list = None
429 if candidates:
430 candidates_list = [v.strip() for v in candidates.split(",")]
432 version_matrix_command(pyproject_path=pyproject, candidates=candidates_list)
435@app.command(name="analyze-benchmarks")
436def analyze_benchmarks(
437 benchmarks_json: Annotated[
438 Path,
439 typer.Option(
440 "--benchmarks-json",
441 help="Path to benchmarks.json file",
442 ),
443 ] = Path("_benchmarks/benchmarks.json"),
444 output_html: Annotated[
445 Path,
446 typer.Option(
447 "--output-html",
448 help="Path to save HTML visualization",
449 ),
450 ] = Path("_benchmarks/benchmarks.html"),
451 verbose: bool = VERBOSE_OPTION,
452) -> None:
453 """Analyze pytest-benchmark results and visualize them.
455 This command reads a benchmarks.json file produced by pytest-benchmark,
456 prints a table with benchmark name, mean milliseconds, and operations per
457 second, and generates an interactive Plotly bar chart of mean runtimes.
459 Note: This command requires pandas and plotly. Install with:
460 uv pip install -e '.[dev]' or pip install 'rhiza-tools[dev]'
462 Args:
463 benchmarks_json: Path to the benchmarks.json file.
464 output_html: Path where the HTML visualization should be saved.
465 verbose: If True, enable verbose debug output.
467 Example:
468 Analyze benchmarks with default paths::
470 $ rhiza-tools analyze-benchmarks
472 Use custom paths::
474 $ rhiza-tools analyze-benchmarks \
475 --benchmarks-json tests/benchmarks.json \
476 --output-html reports/benchmarks.html
477 """
478 _apply_verbose(verbose)
479 analyze_benchmarks_command(benchmarks_json=benchmarks_json, output_html=output_html)