Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/source/reference/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ for the full option list.
## Notes

- 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.
- Input audio is expected to be mono.
- `process` uses the optional `--detection-threshold` override.
Expand Down
17 changes: 17 additions & 0 deletions docs/source/tutorials/run-inference-on-folder.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,23 @@ What this does:
- runs the model on each recording,
- 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.
If you do nothing, BatDetect2 uses the built-in default UK model.

Expand Down
15 changes: 13 additions & 2 deletions src/batdetect2/cli/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""BatDetect2 command line interface."""

from pathlib import Path

import click

from batdetect2.cli.ascii import BATDETECT_ASCII_ART
Expand All @@ -22,8 +24,17 @@
count=True,
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
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.

Use subcommands to run processing, training, evaluation, and dataset
Expand All @@ -37,4 +48,4 @@ def cli(ctx: click.Context, verbose: int = 0):

from batdetect2.logging import enable_logging

enable_logging(verbose)
enable_logging(verbose, log_file=log_file)
7 changes: 6 additions & 1 deletion src/batdetect2/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
]


def enable_logging(level: int):
def enable_logging(level: int, log_file: Path | None = None) -> None:
logger.remove()

if level == 0:
Expand All @@ -61,6 +61,11 @@ def enable_logging(level: int):
log_level = "DEBUG"

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")


Expand Down
39 changes: 39 additions & 0 deletions tests/test_cli/test_base.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,57 @@
"""Behavior-focused tests for top-level CLI command discovery."""

from pathlib import Path

from click.testing import CliRunner

from batdetect2.cli import cli

BASE_DIR = Path(__file__).parent.parent.parent


def test_cli_base_help_lists_main_commands() -> None:
"""User story: discover available workflows from top-level help."""

result = CliRunner().invoke(cli, ["--help"])

assert result.exit_code == 0
assert "--log-file" in result.output
assert "process" in result.output
assert "train" in result.output
assert "evaluate" in result.output
assert "data" 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()
Loading