From 4d8693f143134ddfa5018a8d0dde54f2997cd73d Mon Sep 17 00:00:00 2001 From: Alex Lubbock Date: Thu, 19 Mar 2026 00:07:18 +0000 Subject: [PATCH] feat: add defaults keyword for --mixin `--mixin defaults` expands to the five standard default mixins in place, making it easy to add extras without listing all defaults explicitly: microbench --mixin defaults file-hash -- ./job.sh --- CHANGELOG.md | 6 +++++ docs/cli.md | 16 ++++++++++++-- microbench/cli/main.py | 8 ++++++- microbench/cli/parser.py | 6 ++++- tests/test_cli.py | 48 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cbadac..b99f021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ All notable changes to microbench are documented here. ### Enhancements +- **`--mixin defaults` keyword** (CLI): `defaults` can be used as a mixin + name to expand to the standard default set (`python-info`, `host-info`, + `slurm-info`, `loaded-modules`, `working-dir`). This makes it easy to add + one or more extra mixins without listing all five defaults explicitly: + `microbench --mixin defaults file-hash -- ./job.sh`. + - **`file-hash` mixin — automatic argument file scanning** (CLI): the default hash list now includes not only the command executable (`cmd[0]`) but also any command-line arguments (`cmd[1:]`) that resolve to existing diff --git a/docs/cli.md b/docs/cli.md index a880ac5..a6a05d2 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -133,8 +133,20 @@ mixins with descriptions: microbench --show-mixins ``` -Specifying `--mixin` replaces the defaults entirely. Use `--no-mixin` to -disable all mixins and record only timing and command fields: +Specifying `--mixin` replaces the defaults entirely. Use the special name +`defaults` in a `--mixin` list to include the standard default set, so you +can extend it without listing all of them explicitly: + +```bash +# Defaults plus file-hash +microbench --mixin defaults file-hash -- ./job.sh + +# Defaults plus git-info and cgroup-limits +microbench --mixin defaults git-info cgroup-limits -- ./job.sh +``` + +Use `--no-mixin` to disable all mixins and record only timing and command +fields: ```bash # Only Python info — no host info or SLURM diff --git a/microbench/cli/main.py b/microbench/cli/main.py index 61ab1de..ee5ea11 100644 --- a/microbench/cli/main.py +++ b/microbench/cli/main.py @@ -123,7 +123,13 @@ def main(argv=None): elif args.no_mixins: mixin_names = [] elif args.mixins is not None: - mixin_names = list(dict.fromkeys(args.mixins)) + expanded = [] + for name in args.mixins: + if name == 'defaults': + expanded.extend(_DEFAULT_MIXINS) + else: + expanded.append(name) + mixin_names = list(dict.fromkeys(expanded)) else: mixin_names = list(_DEFAULT_MIXINS) diff --git a/microbench/cli/parser.py b/microbench/cli/parser.py index c891867..9a85f14 100644 --- a/microbench/cli/parser.py +++ b/microbench/cli/parser.py @@ -44,9 +44,11 @@ def _make_mixin_type(mixin_map): """Return an argparse type function that normalises and validates mixin names.""" def _parse(value): + if value == 'defaults': + return 'defaults' canonical = _mb_name_to_cli(value) if value.startswith('MB') else value if canonical not in mixin_map: - valid = ', '.join(sorted(mixin_map)) + valid = 'defaults, ' + ', '.join(sorted(mixin_map)) raise argparse.ArgumentTypeError( f'unknown mixin {value!r}. Available: {valid}' ) @@ -163,6 +165,8 @@ def _build_parser(mixin_map): type=_make_mixin_type(mixin_map), help=( 'One or more mixins to include. Replaces defaults when specified. ' + 'Use the special name "defaults" to include the default set ' + '(e.g. --mixin defaults file-hash). ' 'Use --show-mixins to list available options. ' 'MB-prefixed names (e.g. MBHostInfo) are also accepted.' ), diff --git a/tests/test_cli.py b/tests/test_cli.py index 43b199a..4a0f662 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -125,6 +125,54 @@ def test_cli_explicit_mixin_replaces_defaults(): assert 'slurm' not in record +def test_cli_mixin_defaults_keyword_alone(): + """--mixin defaults alone is equivalent to omitting --mixin.""" + _, record_explicit, _ = _run_main(['--mixin', 'defaults', '--', 'true']) + _, record_implicit, _ = _run_main(['--', 'true']) + + for key in ('python', 'host', 'slurm', 'loaded_modules'): + assert key in record_explicit + assert record_explicit['python']['version'] == record_implicit['python']['version'] + + +def test_cli_mixin_defaults_keyword_extends_defaults(): + """--mixin defaults plus a default mixin on top works.""" + _, record, _ = _run_main(['--mixin', 'defaults', 'working-dir', '--', 'true']) + + # All defaults present + assert 'python' in record + assert 'host' in record + assert 'slurm' in record + assert 'loaded_modules' in record + # working-dir is already in defaults, so no duplicate effect needed — just check + assert 'working_dir' in record['call'] + + +def test_cli_mixin_defaults_keyword_with_extra_mixin(): + """--mixin defaults file-hash produces defaults plus file-hash.""" + _, record, _ = _run_main(['--mixin', 'defaults', 'peak-memory', '--', 'true']) + + assert 'python' in record + assert 'host' in record + # peak-memory records to call.peak_memory_bytes + assert 'peak_memory_bytes' in record['call'] + + +def test_cli_mixin_defaults_keyword_deduplicates(): + """Repeating defaults in --mixin does not produce duplicate mixins.""" + _, record, _ = _run_main(['--mixin', 'defaults', 'defaults', '--', 'true']) + + assert 'python' in record + assert 'host' in record + + +def test_cli_mixin_defaults_keyword_invalid_extra(): + """An unknown mixin alongside defaults exits non-zero.""" + with pytest.raises(SystemExit) as exc: + main(['--mixin', 'defaults', 'no-such-mixin', '--', 'true']) + assert exc.value.code != 0 + + def test_cli_outfile(tmp_path): """--outfile writes JSONL to the specified file.""" outfile = tmp_path / 'results.jsonl'