diff --git a/docs/source/cli.md b/docs/source/cli.md index 45ed0008..cdd07bde 100644 --- a/docs/source/cli.md +++ b/docs/source/cli.md @@ -365,6 +365,19 @@ This prevents partial copies from leaving broken state. See also the [Python API](utilities.md#copying-experiments) for programmatic use. +## Workspaces Management + +You can list the workspaces configured in your `settings.yaml`: + +```bash +experimaestro workspaces list [OPTIONS] +``` + +``` +Options: + `-v`, `--verbose` Output all workspace configuration parameters (e.g. ssh, env, folders) | +``` + ## Cleaning Up Orphans Check for tasks that are not part of any experimental plan in the given diff --git a/src/experimaestro/cli/__init__.py b/src/experimaestro/cli/__init__.py index 2b25f1e9..717f1a68 100644 --- a/src/experimaestro/cli/__init__.py +++ b/src/experimaestro/cli/__init__.py @@ -319,6 +319,11 @@ def get_command(self, ctx, name): cli.add_command(install_skill_cli) +# Import and add workspaces commands +from .workspaces import workspaces as workspaces_cli # noqa: E402 + +cli.add_command(workspaces_cli) + @cli.group() def migrate(): diff --git a/src/experimaestro/cli/workspaces.py b/src/experimaestro/cli/workspaces.py new file mode 100644 index 00000000..4712409a --- /dev/null +++ b/src/experimaestro/cli/workspaces.py @@ -0,0 +1,52 @@ +import click +from termcolor import cprint +from experimaestro.settings import get_settings +from dataclasses import asdict +import yaml + +@click.group() +def workspaces(): + """Manage workspaces defined in settings.yaml""" + pass + +@workspaces.command(name="list") +@click.option("-v", "--verbose", is_flag=True, help="Display all parameters for each workspace") +def list_workspaces(verbose): + """List workspaces configured in settings.yaml""" + settings = get_settings() + + if not settings.workspaces: + cprint("No workspaces configured in settings.yaml", "yellow") + return + + cprint("Experimaestro workspaces:", "white", attrs=["bold"]) + for ws in settings.workspaces: + tags = [] + if ws.is_remote: + tags.append(f"remote: {ws.ssh.host}") + + tags_str = f" [{', '.join(tags)}]" if tags else "" + cprint(f"- {ws.id}: {ws.path}{tags_str}", "cyan") + + if verbose: + # Print a structured representation of all workspace parameters + ws_dict = asdict(ws) + # Remove redundant ID and path from the verbose dump as they are already printed + ws_dict.pop("id", None) + ws_dict.pop("path", None) + # path is a Path object, which might not serialize cleanly with simple yaml dump in older PyYAML, but asdict() might keep it as Path. + # We convert Path to str just to be safe. + def sanitize(d): + for k, v in list(d.items()): + if hasattr(v, '__fspath__'): + d[k] = str(v) + elif isinstance(v, dict): + sanitize(v) + elif isinstance(v, list): + for i, item in enumerate(v): + if hasattr(item, '__fspath__'): + v[i] = str(item) + elif isinstance(item, dict): + sanitize(item) + sanitize(ws_dict) + cprint(yaml.dump(ws_dict, sort_keys=False, default_flow_style=False), "white") diff --git a/src/experimaestro/experiments/grid.py b/src/experimaestro/experiments/grid.py index 8f0ccdd2..1a3d3b89 100644 --- a/src/experimaestro/experiments/grid.py +++ b/src/experimaestro/experiments/grid.py @@ -296,8 +296,9 @@ def converter(value: Any) -> Any: for combination in grid_combinations: cfg_tags = {} new_cfg = copy.deepcopy(base_cfg) - for path, value in zip(param_paths, combination): - cfg_tags[path] = value + for i, (path, value) in enumerate(zip(param_paths, combination)): + if len(value_options[i]) > 1: + cfg_tags[path] = value set_nested_attr(new_cfg, path, value) # Convert any remaining single-value GenericParams to scalars diff --git a/src/experimaestro/tests/test_grid.py b/src/experimaestro/tests/test_grid.py index b2c5e55f..ee1e1a66 100644 --- a/src/experimaestro/tests/test_grid.py +++ b/src/experimaestro/tests/test_grid.py @@ -114,7 +114,17 @@ def test_unique_value_in_tags(): configs, tags = generate_grid(cfg) assert len(configs) == 1 - assert tags[0]["lr"] == 0.1 - assert tags[0]["batch_size"] == 32 + assert tags[0] == {} + + # Test that multi-value params are tagged while single-value params in the same grid are not + cfg_multi = MyConfig(id="test", lr=[0.1, 0.01], batch_size=32) + cfg_multi.lr = GenericParams.from_any(cfg_multi.lr) + cfg_multi.batch_size = GenericParams.from_any(cfg_multi.batch_size) + + configs_multi, tags_multi = generate_grid(cfg_multi) + assert len(configs_multi) == 2 + assert tags_multi[0] == {"lr": 0.1} + assert tags_multi[1] == {"lr": 0.01} + diff --git a/src/experimaestro/tests/test_grid_validation.py b/src/experimaestro/tests/test_grid_validation.py index 7db2336c..73edf0ba 100644 --- a/src/experimaestro/tests/test_grid_validation.py +++ b/src/experimaestro/tests/test_grid_validation.py @@ -79,7 +79,22 @@ def test_unique_value_in_tags_from_validation(): configs, tags = generate_grid(cfg) assert len(configs) == 1 - assert tags[0] == {"lr": 0.05, "sub.value": 10} + assert tags[0] == {} + + # Test with multi-value param + data_multi = { + "id": "test", + "lr": [0.05, 0.1], + "sub": {"value": 10} + } + + cfg_multi = validate_attrs(MainConfig, data_multi) + configs_multi, tags_multi = generate_grid(cfg_multi) + + assert len(configs_multi) == 2 + assert tags_multi[0] == {"lr": 0.05} + assert tags_multi[1] == {"lr": 0.1} + def test_unrecognized_key_in_validation():