feat: add CLI log file output

This commit is contained in:
mbsantiago 2026-06-22 21:17:20 -06:00
parent 920eea690f
commit 0a8787e5f1
3 changed files with 59 additions and 3 deletions

View File

@ -1,5 +1,7 @@
"""BatDetect2 command line interface.""" """BatDetect2 command line interface."""
from pathlib import Path
import click import click
from batdetect2.cli.ascii import BATDETECT_ASCII_ART from batdetect2.cli.ascii import BATDETECT_ASCII_ART
@ -22,8 +24,17 @@ BatDetect2
count=True, count=True,
help="Increase verbosity. -v for INFO, -vv for DEBUG.", help="Increase verbosity. -v for INFO, -vv for DEBUG.",
) )
@click.option(
"--log-file",
type=click.Path(path_type=Path),
help="Write CLI logs to a file in addition to the terminal.",
)
@click.pass_context @click.pass_context
def cli(ctx: click.Context, verbose: int = 0): def cli(
ctx: click.Context,
verbose: int = 0,
log_file: Path | None = None,
) -> None:
"""Run the BatDetect2 CLI. """Run the BatDetect2 CLI.
Use subcommands to run processing, training, evaluation, and dataset Use subcommands to run processing, training, evaluation, and dataset
@ -37,4 +48,4 @@ def cli(ctx: click.Context, verbose: int = 0):
from batdetect2.logging import enable_logging from batdetect2.logging import enable_logging
enable_logging(verbose) enable_logging(verbose, log_file=log_file)

View File

@ -50,7 +50,7 @@ __all__ = [
] ]
def enable_logging(level: int): def enable_logging(level: int, log_file: Path | None = None) -> None:
logger.remove() logger.remove()
if level == 0: if level == 0:
@ -61,6 +61,11 @@ def enable_logging(level: int):
log_level = "DEBUG" log_level = "DEBUG"
logger.add(sys.stderr, level=log_level) logger.add(sys.stderr, level=log_level)
if log_file is not None:
log_file.parent.mkdir(parents=True, exist_ok=True)
logger.add(log_file, level=log_level)
logger.enable("batdetect2") logger.enable("batdetect2")

View File

@ -1,18 +1,58 @@
"""Behavior-focused tests for top-level CLI command discovery.""" """Behavior-focused tests for top-level CLI command discovery."""
from pathlib import Path
from click.testing import CliRunner from click.testing import CliRunner
from batdetect2.cli import cli from batdetect2.cli import cli
BASE_DIR = Path(__file__).parent.parent.parent
def test_cli_base_help_lists_main_commands() -> None: def test_cli_base_help_lists_main_commands() -> None:
"""User story: discover available workflows from top-level help.""" """User story: discover available workflows from top-level help."""
result = CliRunner().invoke(cli, ["--help"]) result = CliRunner().invoke(cli, ["--help"])
assert result.exit_code == 0 assert result.exit_code == 0
assert "--log-file" in result.output
assert "process" in result.output assert "process" in result.output
assert "train" in result.output assert "train" in result.output
assert "evaluate" in result.output assert "evaluate" in result.output
assert "data" in result.output assert "data" in result.output
assert "detect" in result.output assert "detect" in result.output
def test_cli_writes_logs_to_file_and_terminal(
tmp_path: Path,
tiny_checkpoint_path: Path,
) -> None:
"""User story: save CLI logs to a file while keeping terminal output."""
output_dir = tmp_path / "eval_out"
log_file = tmp_path / "logs" / "cli.log"
result = CliRunner().invoke(
cli,
[
"--log-file",
str(log_file),
"-v",
"evaluate",
str(BASE_DIR / "example_data" / "dataset.yaml"),
"--model",
str(tiny_checkpoint_path),
"--base-dir",
str(BASE_DIR),
"--workers",
"0",
"--output-dir",
str(output_dir),
],
)
assert result.exit_code == 0
assert "Initiating evaluation process..." in result.output
assert log_file.exists()
assert "Initiating evaluation process..." in log_file.read_text()