From 683706177285e9ad7ae02e36fe7cc2a7ef05ec32 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Feb 2026 14:45:35 +0000 Subject: [PATCH] feat(cli): add BALATROLLM_CONFIG env var support Add support for specifying the config file path via BALATROLLM_CONFIG environment variable. This allows users to set the config path in their environment instead of passing it as a CLI argument. - Add _resolve_config_path() function to handle precedence logic - CLI positional argument takes precedence over env var - Update docs/cli.md with usage examples and notes - Add unit tests for the new functionality Closes #57 https://claude.ai/code/session_01665SJCL7QdWY2kiettyqV3 --- docs/cli.md | 17 ++++++++++++++ src/balatrollm/cli.py | 19 ++++++++++++++- tests/unit/test_cli.py | 52 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_cli.py diff --git a/docs/cli.md b/docs/cli.md index f9b0618..b66c68d 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -22,6 +22,19 @@ This means CLI flags override config file values, which override environment var | -------- | -------- | ------------------------------- | | `CONFIG` | No | Path to YAML configuration file | +!!! note "BALATROLLM_CONFIG Environment Variable" + + The configuration file path can also be specified via the `BALATROLLM_CONFIG` environment variable: + + ```bash + export BALATROLLM_CONFIG="config/example.yaml" + balatrollm --model openai/gpt-5 + ``` + + This is the **only** `BALATROLLM_*` environment variable that does not have a corresponding CLI flag — the user can simply provide the config file path as a positional argument instead. + + **Precedence:** If both the `CONFIG` argument and `BALATROLLM_CONFIG` are provided, the CLI argument takes precedence and `BALATROLLM_CONFIG` is ignored. + ## Options | CLI Flag | Environment Variable | Default | Description | @@ -59,6 +72,10 @@ balatrollm --model openai/gpt-5 # Run with configuration file balatrollm config/example.yaml +# Run with configuration file via environment variable +export BALATROLLM_CONFIG="config/example.yaml" +balatrollm --model openai/gpt-5 + # Run with config file and override specific options balatrollm config/example.yaml --model openai/gpt-5 --seed BBBBBBB ``` diff --git a/src/balatrollm/cli.py b/src/balatrollm/cli.py index 45465a2..6557e3b 100644 --- a/src/balatrollm/cli.py +++ b/src/balatrollm/cli.py @@ -2,11 +2,15 @@ import argparse import asyncio +import os import sys from pathlib import Path from .config import Config, Task +# Environment variable for config file path (special: no corresponding CLI flag) +BALATROLLM_CONFIG_ENV = "BALATROLLM_CONFIG" + def create_parser() -> argparse.ArgumentParser: """Create the argument parser.""" @@ -83,14 +87,27 @@ async def execute(config: Config, tasks: list[Task]) -> int: views_server.stop() +def _resolve_config_path(args_config: Path | None) -> Path | None: + """Resolve config path: CLI arg takes precedence over BALATROLLM_CONFIG env var.""" + if args_config is not None: + return args_config + env_config = os.environ.get(BALATROLLM_CONFIG_ENV) + if env_config: + return Path(env_config) + return None + + def main() -> None: """Main entry point for balatrollm command.""" parser = create_parser() args = parser.parse_args() + # Resolve config path: CLI arg > BALATROLLM_CONFIG env var + config_path = _resolve_config_path(args.config) + # Build config with precedence: env < yaml < args try: - config = Config.load(yaml_path=args.config, args=args) + config = Config.load(yaml_path=config_path, args=args) config.validate() except (FileNotFoundError, ValueError) as e: parser.error(str(e)) diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py new file mode 100644 index 0000000..53b6a2c --- /dev/null +++ b/tests/unit/test_cli.py @@ -0,0 +1,52 @@ +"""Unit tests for the cli module.""" + +from pathlib import Path + +import pytest + +from balatrollm.cli import BALATROLLM_CONFIG_ENV, _resolve_config_path + +# ============================================================================ +# Test _resolve_config_path +# ============================================================================ + + +class TestResolveConfigPath: + """Tests for _resolve_config_path helper function.""" + + def test_cli_arg_returns_arg(self) -> None: + """CLI argument should be returned when provided.""" + cli_path = Path("/some/config.yaml") + result = _resolve_config_path(cli_path) + assert result == cli_path + + def test_env_var_used_when_no_cli_arg( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """BALATROLLM_CONFIG env var should be used when CLI arg is None.""" + monkeypatch.setenv(BALATROLLM_CONFIG_ENV, "/env/config.yaml") + result = _resolve_config_path(None) + assert result == Path("/env/config.yaml") + + def test_cli_arg_takes_precedence_over_env( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """CLI argument should take precedence over BALATROLLM_CONFIG env var.""" + monkeypatch.setenv(BALATROLLM_CONFIG_ENV, "/env/config.yaml") + cli_path = Path("/cli/config.yaml") + result = _resolve_config_path(cli_path) + assert result == cli_path + + def test_none_when_no_cli_arg_and_no_env( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """None should be returned when neither CLI arg nor env var is set.""" + monkeypatch.delenv(BALATROLLM_CONFIG_ENV, raising=False) + result = _resolve_config_path(None) + assert result is None + + def test_empty_env_var_returns_none(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Empty BALATROLLM_CONFIG env var should be treated as unset.""" + monkeypatch.setenv(BALATROLLM_CONFIG_ENV, "") + result = _resolve_config_path(None) + assert result is None