Merge pull request #71 from macaodha/enhancement/logs-to-file
Some checks are pending
CI / Checks (push) Waiting to run
CI / Tests (Python ${{ matrix.python-version }}) (3.10) (push) Waiting to run
CI / Tests (Python ${{ matrix.python-version }}) (3.11) (push) Waiting to run
CI / Tests (Python ${{ matrix.python-version }}) (3.12) (push) Waiting to run
Docs Pages / Build Docs (push) Waiting to run
Docs Pages / Deploy Docs (push) Blocked by required conditions

Add CLI log file output
This commit is contained in:
Santiago Martinez Balvanera 2026-06-22 21:39:12 -06:00 committed by GitHub
commit 833db23855
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 77 additions and 3 deletions

View File

@ -17,6 +17,8 @@ for the full option list.
## Notes ## Notes
- Global CLI options are documented in {doc}`base`. - Global CLI options are documented in {doc}`base`.
- Use `--log-file path/to/cli.log` to save CLI logs to a file while still
showing them in the terminal.
- Paths with spaces should be wrapped in quotes. - Paths with spaces should be wrapped in quotes.
- Input audio is expected to be mono. - Input audio is expected to be mono.
- `process` uses the optional `--detection-threshold` override. - `process` uses the optional `--detection-threshold` override.

View File

@ -97,6 +97,23 @@ What this does:
- runs the model on each recording, - runs the model on each recording,
- saves the results in `path/to/outputs`. - saves the results in `path/to/outputs`.
```{eval-rst}
.. admonition:: Save CLI logs to a file
:collapsible: closed
:class: tip dropdown
If you want to keep a copy of the CLI logs, add ``--log-file`` before the
subcommand:
.. code-block:: bash
batdetect2 --log-file path/to/cli.log process directory \
path/to/audio \
path/to/outputs
This writes the same CLI logs to ``path/to/cli.log`` and to the terminal.
```
You do not need to choose a model for this first run. You do not need to choose a model for this first run.
If you do nothing, BatDetect2 uses the built-in default UK model. If you do nothing, BatDetect2 uses the built-in default UK model.

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,9 +1,13 @@
"""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."""
@ -11,8 +15,43 @@ def test_cli_base_help_lists_main_commands() -> None:
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()