Skip to content

Commit 95772de

Browse files
Fixes CLI docs tests and deployment to use single JSONL lists file.
1 parent 3f4dee0 commit 95772de

10 files changed

Lines changed: 1568 additions & 1506 deletions

File tree

bead/cli/deployment.py

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def deployment() -> None:
4141
4242
\b
4343
Examples:
44-
$ bead deployment generate lists/ items.jsonl experiment/
44+
$ bead deployment generate lists.jsonl items.jsonl experiment/
4545
$ bead deployment export-jatos experiment/ study.jzip \\
4646
--title "My Study"
4747
$ bead deployment upload-jatos study.jzip \\
@@ -52,9 +52,9 @@ def deployment() -> None:
5252

5353
@click.command()
5454
@click.argument(
55-
"lists_dir", type=click.Path(exists=True, file_okay=False, path_type=Path)
55+
"lists_file", type=click.Path(exists=True, dir_okay=False, path_type=Path)
5656
)
57-
@click.argument("items_file", type=click.Path(exists=True, path_type=Path))
57+
@click.argument("items_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
5858
@click.argument("output_dir", type=click.Path(path_type=Path))
5959
@click.option(
6060
"--experiment-type",
@@ -128,7 +128,7 @@ def deployment() -> None:
128128
@click.pass_context
129129
def generate(
130130
ctx: click.Context,
131-
lists_dir: Path,
131+
lists_file: Path,
132132
items_file: Path,
133133
output_dir: Path,
134134
experiment_type: str,
@@ -148,10 +148,10 @@ def generate(
148148
----------
149149
ctx : click.Context
150150
Click context object.
151-
lists_dir : Path
152-
Directory containing experiment list files.
151+
lists_file : Path
152+
JSONL file containing experiment lists (one list per line).
153153
items_file : Path
154-
Path to items file.
154+
JSONL file containing items (one item per line).
155155
output_dir : Path
156156
Output directory for generated experiment.
157157
experiment_type : str
@@ -178,25 +178,25 @@ def generate(
178178
Examples
179179
--------
180180
# Basic balanced distribution
181-
$ bead deployment generate lists/ items.jsonl experiment/ \\
181+
$ bead deployment generate lists.jsonl items.jsonl experiment/ \\
182182
--experiment-type forced_choice \\
183183
--title "Acceptability Study" \\
184184
--distribution-strategy balanced
185185
186186
# Quota-based with config
187-
$ bead deployment generate lists/ items.jsonl experiment/ \\
187+
$ bead deployment generate lists.jsonl items.jsonl experiment/ \\
188188
--experiment-type forced_choice \\
189189
--distribution-strategy quota_based \\
190190
--distribution-config '{"participants_per_list": 25, "allow_overflow": false}'
191191
192192
# Stratified by factors
193-
$ bead deployment generate lists/ items.jsonl experiment/ \\
193+
$ bead deployment generate lists.jsonl items.jsonl experiment/ \\
194194
--experiment-type forced_choice \\
195195
--distribution-strategy stratified \\
196196
--distribution-config '{"factors": ["condition", "verb_type"]}'
197197
198198
# Dry run to preview
199-
$ bead deployment generate lists/ items.jsonl experiment/ \\
199+
$ bead deployment generate lists.jsonl items.jsonl experiment/ \\
200200
--experiment-type forced_choice \\
201201
--distribution-strategy balanced \\
202202
--dry-run
@@ -227,21 +227,21 @@ def generate(
227227
except ValueError as e:
228228
print_error(f"Invalid distribution strategy configuration: {e}")
229229
ctx.exit(1)
230-
# Load experiment lists
231-
print_info(f"Loading experiment lists from {lists_dir}")
232-
list_files = list(lists_dir.glob("*.jsonl"))
233-
if not list_files:
234-
print_error(f"No list files found in {lists_dir}")
235-
ctx.exit(1)
236-
230+
# Load experiment lists from JSONL file (one list per line)
231+
print_info(f"Loading experiment lists from {lists_file}")
237232
experiment_lists: list[ExperimentList] = []
238-
for list_file in list_files:
239-
with open(list_file, encoding="utf-8") as f:
240-
first_line = f.readline().strip()
241-
if first_line:
242-
list_data = json.loads(first_line)
243-
exp_list = ExperimentList(**list_data)
244-
experiment_lists.append(exp_list)
233+
with open(lists_file, encoding="utf-8") as f:
234+
for line in f:
235+
line = line.strip()
236+
if not line:
237+
continue
238+
list_data = json.loads(line)
239+
exp_list = ExperimentList(**list_data)
240+
experiment_lists.append(exp_list)
241+
242+
if not experiment_lists:
243+
print_error(f"No lists found in {lists_file}")
244+
ctx.exit(1)
245245

246246
print_info(f"Loaded {len(experiment_lists)} experiment lists")
247247

bead/cli/main.py

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -368,31 +368,64 @@ def _generate_gitignore() -> str:
368368
"""
369369

370370

371-
# Import command groups
372-
from bead.cli.active_learning import active_learning # noqa: E402
373-
from bead.cli.completion import completion # noqa: E402
374-
from bead.cli.config import config # noqa: E402
375-
from bead.cli.deployment import deployment # noqa: E402
376-
from bead.cli.items import items # noqa: E402
377-
from bead.cli.lists import lists # noqa: E402
378-
from bead.cli.models import models # noqa: E402
379-
from bead.cli.resources import resources # noqa: E402
380-
from bead.cli.shell import shell # noqa: E402
381-
from bead.cli.simulate import simulate # noqa: E402
382-
from bead.cli.templates import templates # noqa: E402
383-
from bead.cli.training import training # noqa: E402
384-
from bead.cli.workflow import workflow # noqa: E402
385-
386-
cli.add_command(active_learning)
387-
cli.add_command(completion)
388-
cli.add_command(config)
389-
cli.add_command(resources)
390-
cli.add_command(templates)
391-
cli.add_command(items)
392-
cli.add_command(lists)
393-
cli.add_command(deployment)
394-
cli.add_command(models)
395-
cli.add_command(simulate)
396-
cli.add_command(shell)
397-
cli.add_command(training)
398-
cli.add_command(workflow)
371+
# Lazy command loading for fast startup
372+
# Each command group is only imported when actually invoked
373+
374+
375+
class LazyGroup(click.Group):
376+
"""Click group that lazily loads subcommands."""
377+
378+
def __init__(
379+
self,
380+
name: str | None = None,
381+
lazy_subcommands: dict[str, tuple[str, str]] | None = None,
382+
**kwargs: object,
383+
) -> None:
384+
super().__init__(name=name, **kwargs)
385+
self._lazy_subcommands: dict[str, tuple[str, str]] = lazy_subcommands or {}
386+
387+
def list_commands(self, ctx: click.Context) -> list[str]:
388+
"""List all available commands."""
389+
base = super().list_commands(ctx)
390+
lazy = list(self._lazy_subcommands.keys())
391+
return sorted(base + lazy)
392+
393+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
394+
"""Get a command, loading it lazily if needed."""
395+
if cmd_name in self._lazy_subcommands:
396+
return self._lazy_load(cmd_name)
397+
return super().get_command(ctx, cmd_name)
398+
399+
def _lazy_load(self, cmd_name: str) -> click.Command:
400+
"""Import and return a lazy command."""
401+
import importlib
402+
403+
module_path, attr_name = self._lazy_subcommands[cmd_name]
404+
module = importlib.import_module(module_path)
405+
return getattr(module, attr_name) # type: ignore[no-any-return]
406+
407+
408+
# Replace cli group with lazy version
409+
cli = LazyGroup(
410+
name="bead",
411+
help=cli.help,
412+
params=cli.params,
413+
callback=cli.callback,
414+
lazy_subcommands={
415+
"active-learning": ("bead.cli.active_learning", "active_learning"),
416+
"completion": ("bead.cli.completion", "completion"),
417+
"config": ("bead.cli.config", "config"),
418+
"deployment": ("bead.cli.deployment", "deployment"),
419+
"items": ("bead.cli.items", "items"),
420+
"lists": ("bead.cli.lists", "lists"),
421+
"models": ("bead.cli.models", "models"),
422+
"resources": ("bead.cli.resources", "resources"),
423+
"shell": ("bead.cli.shell", "shell"),
424+
"simulate": ("bead.cli.simulate", "simulate"),
425+
"templates": ("bead.cli.templates", "templates"),
426+
"training": ("bead.cli.training", "training"),
427+
"workflow": ("bead.cli.workflow", "workflow"),
428+
},
429+
)
430+
# Re-add the init command which is defined above
431+
cli.add_command(init)

bead/cli/utils.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,15 @@
2222
if TYPE_CHECKING:
2323
from bead.config import BeadConfig
2424

25-
from bead.config import load_config
2625
from bead.data.base import JsonValue
2726

27+
28+
def _load_config() -> "type":
29+
"""Lazily import load_config to avoid slow startup."""
30+
from bead.config import load_config
31+
32+
return load_config
33+
2834
console = Console()
2935

3036

@@ -59,6 +65,7 @@ def load_config_for_cli(
5965
config_path = Path(config_file) if config_file else None
6066

6167
try:
68+
load_config = _load_config()
6269
config = load_config(config_path=config_path, profile=profile)
6370

6471
if verbose:

docs/user-guide/cli/conftest.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
33
This conftest.py sets up the environment for testing bash code blocks
44
in CLI documentation using pytest-codeblocks.
5+
6+
The fixtures are copied to a temporary directory to avoid modifying
7+
the committed fixture files.
58
"""
69

710
import os
811
import shutil
12+
import tempfile
913
from pathlib import Path
1014

1115
import pytest
@@ -14,26 +18,19 @@
1418
DOCS_CLI_DIR = Path(__file__).parent
1519
PROJECT_ROOT = DOCS_CLI_DIR.parent.parent.parent
1620
FIXTURES_SRC = PROJECT_ROOT / "tests" / "fixtures" / "api_docs"
17-
FIXTURES_WORK = PROJECT_ROOT / "tests" / "fixtures" / "cli_work"
1821

22+
# Global temp directory for the test session
23+
_temp_dir: Path | None = None
1924

20-
def pytest_configure(config: pytest.Config) -> None:
21-
"""Set up environment before test collection.
2225

23-
This hook runs before test collection starts. We set up the fixtures
24-
directory but do NOT change the cwd yet, as that would break collection.
25-
"""
26-
# Create working directory and copy fixtures
27-
FIXTURES_WORK.mkdir(parents=True, exist_ok=True)
26+
def pytest_configure(config: pytest.Config) -> None:
27+
"""Set up temporary fixtures directory before test collection."""
28+
global _temp_dir
29+
_temp_dir = Path(tempfile.mkdtemp(prefix="bead_cli_test_"))
2830

31+
# Copy fixtures to temp directory
2932
for item in FIXTURES_SRC.iterdir():
30-
dest = FIXTURES_WORK / item.name
31-
if dest.exists():
32-
if dest.is_dir():
33-
shutil.rmtree(dest)
34-
else:
35-
dest.unlink()
36-
33+
dest = _temp_dir / item.name
3734
if item.is_dir():
3835
shutil.copytree(item, dest)
3936
else:
@@ -47,11 +44,14 @@ def pytest_runtest_setup(item: pytest.Item) -> None:
4744
This hook runs just before each test executes. We change the cwd here
4845
so that bash commands in the markdown files can find the fixture files.
4946
"""
47+
if _temp_dir is None:
48+
return
49+
5050
# Store original directory on the item for restoration
5151
item._original_cwd = os.getcwd() # type: ignore[attr-defined]
5252

53-
# Change to fixtures directory
54-
os.chdir(FIXTURES_WORK)
53+
# Change to temp fixtures directory
54+
os.chdir(_temp_dir)
5555

5656
# Add .venv/bin to PATH for bead CLI
5757
venv_bin = PROJECT_ROOT / ".venv" / "bin"
@@ -72,6 +72,8 @@ def pytest_runtest_teardown(item: pytest.Item) -> None:
7272

7373

7474
def pytest_unconfigure(config: pytest.Config) -> None:
75-
"""Clean up fixtures directory after all tests complete."""
76-
if FIXTURES_WORK.exists():
77-
shutil.rmtree(FIXTURES_WORK, ignore_errors=True)
75+
"""Clean up temporary fixtures directory after all tests complete."""
76+
global _temp_dir
77+
if _temp_dir is not None and _temp_dir.exists():
78+
shutil.rmtree(_temp_dir, ignore_errors=True)
79+
_temp_dir = None

0 commit comments

Comments
 (0)