hydraflow 0.10.0__tar.gz → 0.10.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hydraflow-0.10.2/.cursorrules +6 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/.gitignore +1 -1
- {hydraflow-0.10.0 → hydraflow-0.10.2}/PKG-INFO +1 -1
- {hydraflow-0.10.0 → hydraflow-0.10.2}/pyproject.toml +1 -1
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/cli.py +22 -23
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/core/io.py +1 -1
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/core/main.py +8 -4
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/entities/run_collection.py +1 -1
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/executor/io.py +15 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/executor/job.py +24 -11
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/executor/parser.py +38 -13
- hydraflow-0.10.2/tests/cli/test_run.py +41 -0
- hydraflow-0.10.2/tests/cli/test_show.py +18 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/io/test_hydra_dir.py +1 -1
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/main/test_match_overrides.py +8 -2
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/executor/test_job.py +34 -20
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/executor/test_parser.py +74 -6
- hydraflow-0.10.0/tests/cli/test_run.py +0 -33
- hydraflow-0.10.0/tests/cli/test_show.py +0 -23
- {hydraflow-0.10.0 → hydraflow-0.10.2}/.devcontainer/devcontainer.json +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/.devcontainer/postCreate.sh +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/.devcontainer/starship.toml +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/.gitattributes +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/.github/workflows/ci.yaml +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/.github/workflows/docs.yaml +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/LICENSE +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/README.md +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/apps/quickstart.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/docs/index.md +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/docs/usage/quickstart.md +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/mkdocs.yaml +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/__init__.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/core/__init__.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/core/config.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/core/context.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/core/mlflow.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/core/param.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/entities/__init__.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/entities/run_data.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/entities/run_info.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/executor/__init__.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/executor/conf.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/src/hydraflow/py.typed +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/__init__.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/cli/__init__.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/cli/app.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/cli/conftest.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/cli/hydraflow.yaml +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/cli/test_setup.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/cli/test_version.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/conftest.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/__init__.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/config/__init__.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/config/test_config.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/config/test_params.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/context/__init__.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/context/chdir.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/context/log_run.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/context/start_run.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/context/test_chdir.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/context/test_log_run.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/context/test_start_run.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/io/__init__.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/io/hydra_dir.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/io/test_iter_dirs.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/io/test_run.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/main/__init__.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/main/default.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/main/force_new_run.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/main/match_overrides.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/main/rerun_finished.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/main/skip_finished.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/main/test_default.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/main/test_force_new_run.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/main/test_rerun_finished.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/main/test_skip_finished.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/param/__init__.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/param/params.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/param/test_param.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/param/test_params.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/core/test_mlflow.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/entities/__init__.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/entities/filter.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/entities/test_collection.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/entities/test_data.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/entities/test_filter.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/entities/test_info.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/entities/test_values.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/entities/values.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/executor/__init__.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/executor/conftest.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/executor/echo.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/executor/test_args.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/executor/test_conf.py +0 -0
- {hydraflow-0.10.0 → hydraflow-0.10.2}/tests/executor/test_io.py +0 -0
@@ -0,0 +1,6 @@
|
|
1
|
+
You are an AI assistant specialized in Python development. Your approach emphasizes:
|
2
|
+
|
3
|
+
1. Comprehensive testing with pytest.
|
4
|
+
2. Code style consistency using Ruff.
|
5
|
+
3. Detailed documentation using docstrings and README files.
|
6
|
+
4. Generate Git commit messages in English.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hydraflow
|
3
|
-
Version: 0.10.
|
3
|
+
Version: 0.10.2
|
4
4
|
Summary: Hydraflow integrates Hydra and MLflow to manage and track machine learning experiments.
|
5
5
|
Project-URL: Documentation, https://daizutabi.github.io/hydraflow/
|
6
6
|
Project-URL: Source, https://github.com/daizutabi/hydraflow
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "hydraflow"
|
7
|
-
version = "0.10.
|
7
|
+
version = "0.10.2"
|
8
8
|
description = "Hydraflow integrates Hydra and MLflow to manage and track machine learning experiments."
|
9
9
|
readme = "README.md"
|
10
10
|
license = { file = "LICENSE" }
|
@@ -2,54 +2,53 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
from typing import
|
5
|
+
from typing import Annotated
|
6
6
|
|
7
7
|
import typer
|
8
8
|
from rich.console import Console
|
9
9
|
from typer import Argument, Option
|
10
10
|
|
11
|
-
from hydraflow.executor.io import load_config
|
12
|
-
|
13
|
-
if TYPE_CHECKING:
|
14
|
-
from hydraflow.executor.job import Job
|
11
|
+
from hydraflow.executor.io import get_job, load_config
|
15
12
|
|
16
13
|
app = typer.Typer(add_completion=False)
|
17
14
|
console = Console()
|
18
15
|
|
19
16
|
|
20
|
-
def get_job(name: str) -> Job:
|
21
|
-
cfg = load_config()
|
22
|
-
job = cfg.jobs[name]
|
23
|
-
|
24
|
-
if not job.name:
|
25
|
-
job.name = name
|
26
|
-
|
27
|
-
return job
|
28
|
-
|
29
|
-
|
30
17
|
@app.command()
|
31
18
|
def run(
|
32
19
|
name: Annotated[str, Argument(help="Job name.", show_default=False)],
|
20
|
+
*,
|
21
|
+
dry_run: Annotated[
|
22
|
+
bool,
|
23
|
+
Option("--dry-run", help="Perform a dry run"),
|
24
|
+
] = False,
|
33
25
|
) -> None:
|
34
26
|
"""Run a job."""
|
35
27
|
import mlflow
|
36
28
|
|
37
|
-
from hydraflow.executor.job import multirun
|
29
|
+
from hydraflow.executor.job import multirun, to_text
|
38
30
|
|
39
31
|
job = get_job(name)
|
40
|
-
|
41
|
-
|
32
|
+
if dry_run:
|
33
|
+
typer.echo(to_text(job))
|
34
|
+
else:
|
35
|
+
mlflow.set_experiment(job.name)
|
36
|
+
multirun(job)
|
42
37
|
|
43
38
|
|
44
39
|
@app.command()
|
45
40
|
def show(
|
46
|
-
name: Annotated[str, Argument(help="Job name.", show_default=False)],
|
41
|
+
name: Annotated[str, Argument(help="Job name.", show_default=False)] = "",
|
47
42
|
) -> None:
|
48
|
-
"""Show
|
49
|
-
from
|
43
|
+
"""Show the hydraflow config."""
|
44
|
+
from omegaconf import OmegaConf
|
50
45
|
|
51
|
-
|
52
|
-
|
46
|
+
if name:
|
47
|
+
cfg = get_job(name)
|
48
|
+
else:
|
49
|
+
cfg = load_config()
|
50
|
+
|
51
|
+
typer.echo(OmegaConf.to_yaml(cfg))
|
53
52
|
|
54
53
|
|
55
54
|
@app.callback(invoke_without_command=True)
|
@@ -136,7 +136,7 @@ def load_overrides(run: Run) -> ListConfig:
|
|
136
136
|
|
137
137
|
"""
|
138
138
|
path = get_artifact_dir(run) / ".hydra/overrides.yaml"
|
139
|
-
return OmegaConf.load(path) # type: ignore
|
139
|
+
return sorted(OmegaConf.load(path)) # type: ignore
|
140
140
|
|
141
141
|
|
142
142
|
def remove_run(run: Run | Iterable[Run]) -> None:
|
@@ -154,11 +154,15 @@ def equals(run_dir: Path, config: Any, overrides: list[str] | None) -> bool:
|
|
154
154
|
"""
|
155
155
|
if overrides is None:
|
156
156
|
path = run_dir / "artifacts/.hydra/config.yaml"
|
157
|
-
|
158
|
-
|
159
|
-
|
157
|
+
|
158
|
+
if not path.exists():
|
159
|
+
return False
|
160
|
+
|
161
|
+
return OmegaConf.load(path) == config
|
162
|
+
|
163
|
+
path = run_dir / "artifacts/.hydra/overrides.yaml"
|
160
164
|
|
161
165
|
if not path.exists():
|
162
166
|
return False
|
163
167
|
|
164
|
-
return OmegaConf.load(path) ==
|
168
|
+
return sorted(OmegaConf.load(path)) == sorted(overrides)
|
@@ -116,7 +116,7 @@ class RunCollection:
|
|
116
116
|
but not in the other.
|
117
117
|
|
118
118
|
"""
|
119
|
-
runs = [run for run in self._runs if run not in other._runs]
|
119
|
+
runs = [run for run in self._runs if run not in other._runs]
|
120
120
|
return self.__class__(runs)
|
121
121
|
|
122
122
|
@property
|
@@ -3,11 +3,15 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
from pathlib import Path
|
6
|
+
from typing import TYPE_CHECKING
|
6
7
|
|
7
8
|
from omegaconf import OmegaConf
|
8
9
|
|
9
10
|
from .conf import HydraflowConf
|
10
11
|
|
12
|
+
if TYPE_CHECKING:
|
13
|
+
from .job import Job
|
14
|
+
|
11
15
|
|
12
16
|
def find_config_file() -> Path | None:
|
13
17
|
"""Find the hydraflow config file."""
|
@@ -32,3 +36,14 @@ def load_config() -> HydraflowConf:
|
|
32
36
|
cfg = OmegaConf.load(path)
|
33
37
|
|
34
38
|
return OmegaConf.merge(schema, cfg) # type: ignore
|
39
|
+
|
40
|
+
|
41
|
+
def get_job(name: str) -> Job:
|
42
|
+
"""Get a job from the config."""
|
43
|
+
cfg = load_config()
|
44
|
+
job = cfg.jobs[name]
|
45
|
+
|
46
|
+
if not job.name:
|
47
|
+
job.name = name
|
48
|
+
|
49
|
+
return job
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
20
20
|
import importlib
|
21
21
|
import shlex
|
22
22
|
import subprocess
|
23
|
+
import sys
|
23
24
|
from subprocess import CalledProcessError
|
24
25
|
from typing import TYPE_CHECKING
|
25
26
|
|
@@ -99,6 +100,9 @@ def multirun(job: Job) -> None:
|
|
99
100
|
|
100
101
|
if job.run:
|
101
102
|
base_cmds = shlex.split(job.run)
|
103
|
+
if base_cmds[0] == "python" and sys.platform == "win32":
|
104
|
+
base_cmds[0] = sys.executable
|
105
|
+
|
102
106
|
for args in it:
|
103
107
|
cmds = [*base_cmds, *args]
|
104
108
|
try:
|
@@ -108,45 +112,54 @@ def multirun(job: Job) -> None:
|
|
108
112
|
raise RuntimeError(msg) from e
|
109
113
|
|
110
114
|
elif job.call:
|
111
|
-
|
112
|
-
|
115
|
+
call_name, *base_args = shlex.split(job.call)
|
116
|
+
|
117
|
+
if "." not in call_name:
|
118
|
+
msg = f"Invalid function path: {call_name}."
|
113
119
|
msg += " Expected format: 'package.module.function'"
|
114
120
|
raise ValueError(msg)
|
115
121
|
|
116
122
|
try:
|
117
|
-
module_name, func_name =
|
123
|
+
module_name, func_name = call_name.rsplit(".", 1)
|
118
124
|
module = importlib.import_module(module_name)
|
119
125
|
func = getattr(module, func_name)
|
120
126
|
except (ImportError, AttributeError, ModuleNotFoundError) as e:
|
121
|
-
msg = f"Failed to import or find function: {
|
127
|
+
msg = f"Failed to import or find function: {call_name}"
|
122
128
|
raise ValueError(msg) from e
|
123
129
|
|
124
130
|
for args in it:
|
125
131
|
try:
|
126
|
-
func(*args)
|
132
|
+
func([*base_args, *args])
|
127
133
|
except Exception as e: # noqa: PERF203
|
128
134
|
msg = f"Function call '{job.call}' failed with args: {args}"
|
129
135
|
raise RuntimeError(msg) from e
|
130
136
|
|
131
137
|
|
132
|
-
def
|
133
|
-
"""
|
138
|
+
def to_text(job: Job) -> str:
|
139
|
+
"""Convert the job configuration to a string.
|
134
140
|
|
135
|
-
This function
|
141
|
+
This function returns the job configuration for a given job.
|
136
142
|
|
137
143
|
Args:
|
138
144
|
job (Job): The job configuration to show.
|
139
145
|
|
146
|
+
Returns:
|
147
|
+
str: The job configuration.
|
148
|
+
|
140
149
|
"""
|
150
|
+
text = ""
|
151
|
+
|
141
152
|
it = iter_batches(job)
|
142
153
|
|
143
154
|
if job.run:
|
144
155
|
base_cmds = shlex.split(job.run)
|
145
156
|
for args in it:
|
146
157
|
cmds = " ".join([*base_cmds, *args])
|
147
|
-
|
158
|
+
text += f"{cmds}\n"
|
148
159
|
|
149
160
|
elif job.call:
|
150
|
-
|
161
|
+
text = f"call: {job.call}\n"
|
151
162
|
for args in it:
|
152
|
-
|
163
|
+
text += f"args: {args}\n"
|
164
|
+
|
165
|
+
return text
|
@@ -8,7 +8,7 @@ ranges, and expand values from string arguments.
|
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
10
|
|
11
|
-
import
|
11
|
+
import shlex
|
12
12
|
from itertools import chain, product
|
13
13
|
from typing import TYPE_CHECKING
|
14
14
|
|
@@ -289,7 +289,7 @@ def split(arg: str) -> list[str]:
|
|
289
289
|
return result
|
290
290
|
|
291
291
|
|
292
|
-
def expand_values(arg: str) ->
|
292
|
+
def expand_values(arg: str, suffix: str = "") -> Iterator[str]:
|
293
293
|
"""Expand a string argument into a list of values.
|
294
294
|
|
295
295
|
Take a string containing comma-separated values or ranges and return a list
|
@@ -297,12 +297,36 @@ def expand_values(arg: str) -> list[str]:
|
|
297
297
|
|
298
298
|
Args:
|
299
299
|
arg (str): The argument to expand.
|
300
|
+
suffix (str): The suffix to append to each value.
|
300
301
|
|
301
302
|
Returns:
|
302
|
-
|
303
|
+
Iterator[str]: An iterator of the expanded values.
|
303
304
|
|
304
305
|
"""
|
305
|
-
|
306
|
+
if suffix in SUFFIX_EXPONENT:
|
307
|
+
suffix = SUFFIX_EXPONENT[suffix]
|
308
|
+
|
309
|
+
for value in chain.from_iterable(collect_values(x) for x in split(arg)):
|
310
|
+
yield f"{value}{suffix}"
|
311
|
+
|
312
|
+
|
313
|
+
def split_arg(arg: str) -> tuple[str, str, str]:
|
314
|
+
"""Split an argument into a key, suffix, and value.
|
315
|
+
|
316
|
+
Args:
|
317
|
+
arg (str): The argument to split.
|
318
|
+
|
319
|
+
Returns:
|
320
|
+
tuple[str, str, str]: A tuple containing the key, suffix, and value.
|
321
|
+
|
322
|
+
"""
|
323
|
+
key, value = arg.split("=")
|
324
|
+
|
325
|
+
if "/" in key:
|
326
|
+
key, suffix = key.split("/", 1)
|
327
|
+
return key, suffix, value
|
328
|
+
|
329
|
+
return key, "", value
|
306
330
|
|
307
331
|
|
308
332
|
def collect_arg(arg: str) -> str:
|
@@ -318,9 +342,9 @@ def collect_arg(arg: str) -> str:
|
|
318
342
|
str: A string of the collected key and values.
|
319
343
|
|
320
344
|
"""
|
321
|
-
key,
|
322
|
-
|
323
|
-
return f"{key}={
|
345
|
+
key, suffix, value = split_arg(arg)
|
346
|
+
value = ",".join(expand_values(value, suffix))
|
347
|
+
return f"{key}={value}"
|
324
348
|
|
325
349
|
|
326
350
|
def expand_arg(arg: str) -> Iterator[str]:
|
@@ -338,26 +362,27 @@ def expand_arg(arg: str) -> Iterator[str]:
|
|
338
362
|
|
339
363
|
"""
|
340
364
|
if "|" not in arg:
|
341
|
-
key, value = arg
|
365
|
+
key, suffix, value = split_arg(arg)
|
342
366
|
|
343
|
-
for v in expand_values(value):
|
367
|
+
for v in expand_values(value, suffix):
|
344
368
|
yield f"{key}={v}"
|
345
369
|
|
346
370
|
return
|
347
371
|
|
348
372
|
args = arg.split("|")
|
349
373
|
key = ""
|
374
|
+
suffix = ""
|
350
375
|
|
351
376
|
for arg_ in args:
|
352
377
|
if "=" in arg_:
|
353
|
-
key, value = arg_
|
378
|
+
key, suffix, value = split_arg(arg_)
|
354
379
|
elif key:
|
355
380
|
value = arg_
|
356
381
|
else:
|
357
382
|
msg = f"Invalid argument: {arg_}"
|
358
383
|
raise ValueError(msg)
|
359
384
|
|
360
|
-
value = ",".join(expand_values(value))
|
385
|
+
value = ",".join(expand_values(value, suffix))
|
361
386
|
yield f"{key}={value}"
|
362
387
|
|
363
388
|
|
@@ -372,7 +397,7 @@ def collect(args: str | list[str]) -> list[str]:
|
|
372
397
|
|
373
398
|
"""
|
374
399
|
if isinstance(args, str):
|
375
|
-
args =
|
400
|
+
args = shlex.split(args)
|
376
401
|
|
377
402
|
args = [arg for arg in args if "=" in arg]
|
378
403
|
|
@@ -390,7 +415,7 @@ def expand(args: str | list[str]) -> list[list[str]]:
|
|
390
415
|
|
391
416
|
"""
|
392
417
|
if isinstance(args, str):
|
393
|
-
args =
|
418
|
+
args = shlex.split(args)
|
394
419
|
|
395
420
|
args = [arg for arg in args if "=" in arg]
|
396
421
|
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import pytest
|
2
|
+
from typer.testing import CliRunner
|
3
|
+
|
4
|
+
import hydraflow
|
5
|
+
from hydraflow.cli import app
|
6
|
+
|
7
|
+
pytestmark = pytest.mark.xdist_group(name="group1")
|
8
|
+
|
9
|
+
runner = CliRunner()
|
10
|
+
|
11
|
+
|
12
|
+
def test_run_args():
|
13
|
+
result = runner.invoke(app, ["run", "args"])
|
14
|
+
assert result.exit_code == 0
|
15
|
+
run_ids = hydraflow.list_run_ids("args")
|
16
|
+
assert len(run_ids) == 12
|
17
|
+
|
18
|
+
|
19
|
+
def test_run_batch():
|
20
|
+
result = runner.invoke(app, ["run", "batch"])
|
21
|
+
assert result.exit_code == 0
|
22
|
+
run_ids = hydraflow.list_run_ids("batch")
|
23
|
+
assert len(run_ids) == 8
|
24
|
+
|
25
|
+
|
26
|
+
def test_run_args_dry_run():
|
27
|
+
result = runner.invoke(app, ["run", "args", "--dry-run"])
|
28
|
+
assert result.exit_code == 0
|
29
|
+
assert "hydra.job.name=args" in result.stdout
|
30
|
+
assert "count=1,2,3 name=a,b" in result.stdout
|
31
|
+
assert "count=4,5,6 name=c,d" in result.stdout
|
32
|
+
|
33
|
+
|
34
|
+
def test_run_batch_dry_run():
|
35
|
+
result = runner.invoke(app, ["run", "batch", "--dry-run"])
|
36
|
+
assert result.exit_code == 0
|
37
|
+
assert "hydra.job.name=batch" in result.stdout
|
38
|
+
assert "count=1,2 name=a" in result.stdout
|
39
|
+
assert "count=1,2 name=b" in result.stdout
|
40
|
+
assert "count=100 name=c,d" in result.stdout
|
41
|
+
assert "count=100 name=e,f" in result.stdout
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from typer.testing import CliRunner
|
2
|
+
|
3
|
+
from hydraflow.cli import app
|
4
|
+
|
5
|
+
runner = CliRunner()
|
6
|
+
|
7
|
+
|
8
|
+
def test_show():
|
9
|
+
result = runner.invoke(app, ["show"])
|
10
|
+
assert result.exit_code == 0
|
11
|
+
assert "jobs:\n" in result.stdout
|
12
|
+
assert " args:\n" in result.stdout
|
13
|
+
|
14
|
+
|
15
|
+
def test_show_job():
|
16
|
+
result = runner.invoke(app, ["show", "args"])
|
17
|
+
assert result.exit_code == 0
|
18
|
+
assert "name: args\n" in result.stdout
|
@@ -11,8 +11,8 @@ pytestmark = pytest.mark.xdist_group(name="group5")
|
|
11
11
|
def rc(collect):
|
12
12
|
file = Path(__file__).parent / "match_overrides.py"
|
13
13
|
collect(file, ["-m", "count=1,2"])
|
14
|
-
collect(file, ["-m", "name=a,b"])
|
15
|
-
return collect(file, ["-m", "name=a,b"])
|
14
|
+
collect(file, ["-m", "name=a,b", "count=1"])
|
15
|
+
return collect(file, ["-m", "count=1", "name=a,b"])
|
16
16
|
|
17
17
|
|
18
18
|
def test_rc_len(rc: RunCollection):
|
@@ -23,3 +23,9 @@ def test_config(rc: RunCollection):
|
|
23
23
|
df = rc.data.config
|
24
24
|
assert len(df) == 4
|
25
25
|
assert len(df.drop_duplicates()) == 3
|
26
|
+
|
27
|
+
|
28
|
+
def test_equals():
|
29
|
+
from hydraflow.core.main import equals
|
30
|
+
|
31
|
+
assert equals(Path.cwd(), None, []) is False
|
@@ -71,17 +71,13 @@ def test_sweep_args(batches, i, x):
|
|
71
71
|
assert batches[i][-2] == x
|
72
72
|
|
73
73
|
|
74
|
-
@pytest.mark.skipif(
|
75
|
-
sys.platform == "win32",
|
76
|
-
reason="Windows does not support this test",
|
77
|
-
)
|
78
74
|
def test_multirun_run(job: Job, tmp_path: Path):
|
79
75
|
from hydraflow.executor.job import multirun
|
80
76
|
|
81
77
|
path = tmp_path / "output.txt"
|
82
78
|
file = Path(__file__).parent / "echo.py"
|
83
79
|
|
84
|
-
job.run = f"
|
80
|
+
job.run = f"python {file.as_posix()} {path.as_posix()}"
|
85
81
|
multirun(job)
|
86
82
|
assert path.read_text() == "a=1,2 b=5 a=1,2 b=6 a=3,4 c=7 a=3,4 c=8"
|
87
83
|
|
@@ -89,40 +85,58 @@ def test_multirun_run(job: Job, tmp_path: Path):
|
|
89
85
|
def test_multirun_run_error(job: Job):
|
90
86
|
from hydraflow.executor.job import multirun
|
91
87
|
|
92
|
-
job.run = "false"
|
88
|
+
job.run = "cmd /c exit 1" if sys.platform == "win32" else "false"
|
93
89
|
with pytest.raises(RuntimeError):
|
94
90
|
multirun(job)
|
95
91
|
|
96
92
|
|
97
|
-
def
|
93
|
+
def test_multirun_call(job: Job, capsys: pytest.CaptureFixture):
|
98
94
|
from hydraflow.executor.job import multirun
|
99
95
|
|
100
|
-
job.call = "
|
101
|
-
|
96
|
+
job.call = "typer.echo"
|
97
|
+
multirun(job)
|
98
|
+
out, _ = capsys.readouterr()
|
99
|
+
assert "'a=1,2', 'b=5'" in out
|
100
|
+
assert "'a=3,4', 'c=8'" in out
|
101
|
+
|
102
|
+
|
103
|
+
def test_multirun_call_args(job: Job, capsys: pytest.CaptureFixture):
|
104
|
+
from hydraflow.executor.job import multirun
|
105
|
+
|
106
|
+
job.call = "typer.echo a 'b c'"
|
107
|
+
multirun(job)
|
108
|
+
out, _ = capsys.readouterr()
|
109
|
+
assert "['a', 'b c', '--multirun'," in out
|
110
|
+
|
111
|
+
|
112
|
+
def test_multirun_call_error(job: Job):
|
113
|
+
from hydraflow.executor.job import multirun
|
114
|
+
|
115
|
+
job.call = "hydraflow.executor.job.multirun"
|
116
|
+
with pytest.raises(RuntimeError):
|
102
117
|
multirun(job)
|
103
118
|
|
104
119
|
|
105
|
-
def
|
120
|
+
def test_multirun_call_invalid(job: Job):
|
106
121
|
from hydraflow.executor.job import multirun
|
107
122
|
|
108
|
-
job.call = "
|
123
|
+
job.call = "print"
|
109
124
|
with pytest.raises(ValueError):
|
110
125
|
multirun(job)
|
111
126
|
|
112
127
|
|
113
|
-
def
|
128
|
+
def test_multirun_call_not_found(job: Job):
|
114
129
|
from hydraflow.executor.job import multirun
|
115
130
|
|
116
|
-
job.call = "
|
117
|
-
with pytest.raises(
|
131
|
+
job.call = "hydraflow.invalid"
|
132
|
+
with pytest.raises(ValueError):
|
118
133
|
multirun(job)
|
119
134
|
|
120
135
|
|
121
|
-
def
|
122
|
-
from hydraflow.executor.job import
|
136
|
+
def test_to_text(job: Job):
|
137
|
+
from hydraflow.executor.job import to_text
|
123
138
|
|
124
139
|
job.call = "typer.echo"
|
125
|
-
|
126
|
-
|
127
|
-
assert "
|
128
|
-
assert "'hydra.job.name=test', 'a=3,4', 'c=8']" in out
|
140
|
+
text = to_text(job)
|
141
|
+
assert "call: typer.echo\n" in text
|
142
|
+
assert "'hydra.job.name=test', 'a=3,4', 'c=8']" in text
|
@@ -129,18 +129,37 @@ def test_collect_value(s, x):
|
|
129
129
|
def test_expand_value(s, x):
|
130
130
|
from hydraflow.executor.parser import expand_values
|
131
131
|
|
132
|
-
assert expand_values(s) == x
|
132
|
+
assert list(expand_values(s)) == x
|
133
|
+
|
134
|
+
|
135
|
+
@pytest.mark.parametrize(
|
136
|
+
("s", "x"),
|
137
|
+
[
|
138
|
+
("1,2,3", ["1e3", "2e3", "3e3"]),
|
139
|
+
("1:3,5:6", ["1e3", "2e3", "3e3", "5e3", "6e3"]),
|
140
|
+
("0:0.25:1,2.0", ["0e3", "0.25e3", "0.5e3", "0.75e3", "1.0e3", "2.0e3"]),
|
141
|
+
("3", ["3e3"]),
|
142
|
+
],
|
143
|
+
)
|
144
|
+
def test_expand_value_suffix(s, x):
|
145
|
+
from hydraflow.executor.parser import expand_values
|
146
|
+
|
147
|
+
assert list(expand_values(s, "k")) == x
|
133
148
|
|
134
149
|
|
135
150
|
@pytest.mark.parametrize(
|
136
151
|
("s", "x"),
|
137
152
|
[
|
138
153
|
("a=1", "a=1"),
|
154
|
+
("a/M=1", "a=1e6"),
|
139
155
|
("a=1,2", "a=1,2"),
|
156
|
+
("a/n=1,2", "a=1e-9,2e-9"),
|
140
157
|
("a=1:2", "a=1,2"),
|
158
|
+
("a/M=1:2", "a=1e6,2e6"),
|
141
159
|
("a=:2:3", "a=0,2"),
|
142
160
|
("a=1:3:k", "a=1e3,2e3,3e3"),
|
143
161
|
("a=1:3:k,2:4:M", "a=1e3,2e3,3e3,2e6,3e6,4e6"),
|
162
|
+
("a/m=1:3,8:10", "a=1e-3,2e-3,3e-3,8e-3,9e-3,10e-3"),
|
144
163
|
],
|
145
164
|
)
|
146
165
|
def test_collect_arg(s, x):
|
@@ -154,13 +173,17 @@ def test_collect_arg(s, x):
|
|
154
173
|
[
|
155
174
|
("a=1", ["a=1"]),
|
156
175
|
("a=1,2", ["a=1", "a=2"]),
|
176
|
+
("a/M=1,2", ["a=1e6", "a=2e6"]),
|
157
177
|
("a=1:2", ["a=1", "a=2"]),
|
178
|
+
("a/n=1:2", ["a=1e-9", "a=2e-9"]),
|
158
179
|
("a=:2:3", ["a=0", "a=2"]),
|
159
180
|
("a=1:3:k", ["a=1e3", "a=2e3", "a=3e3"]),
|
160
181
|
("a=1:3:k,2:4:M", ["a=1e3", "a=2e3", "a=3e3", "a=2e6", "a=3e6", "a=4e6"]),
|
161
182
|
("a=1,2|3,4", ["a=1,2", "a=3,4"]),
|
183
|
+
("a/G=1,2|3,4", ["a=1e9,2e9", "a=3e9,4e9"]),
|
162
184
|
("a=1:4|3:5:m", ["a=1,2,3,4", "a=3e-3,4e-3,5e-3"]),
|
163
185
|
("a=1,2|b=3,4|c=5,6", ["a=1,2", "b=3,4", "c=5,6"]),
|
186
|
+
("a/k=1,2|b/m=3,4|c/u=5,6", ["a=1e3,2e3", "b=3e-3,4e-3", "c=5e-6,6e-6"]),
|
164
187
|
],
|
165
188
|
)
|
166
189
|
def test_expand_arg(s, x):
|
@@ -179,14 +202,29 @@ def test_expand_arg_error():
|
|
179
202
|
@pytest.mark.parametrize(
|
180
203
|
("s", "x"),
|
181
204
|
[
|
182
|
-
(["a=1"], ["a=1"]),
|
205
|
+
(["a=1", "b"], ["a=1"]),
|
183
206
|
(["a=1:3"], ["a=1,2,3"]),
|
207
|
+
(["a/m=1:3"], ["a=1e-3,2e-3,3e-3"]),
|
184
208
|
(["a=1:3", "b=4:6"], ["a=1,2,3", "b=4,5,6"]),
|
209
|
+
(["a/k=1:3", "b/m=4:6"], ["a=1e3,2e3,3e3", "b=4e-3,5e-3,6e-3"]),
|
210
|
+
],
|
211
|
+
)
|
212
|
+
def test_collect_list(s, x):
|
213
|
+
from hydraflow.executor.parser import collect
|
214
|
+
|
215
|
+
assert collect(s) == x
|
216
|
+
|
217
|
+
|
218
|
+
@pytest.mark.parametrize(
|
219
|
+
("s", "x"),
|
220
|
+
[
|
185
221
|
("a=1:3\nb=4:6", ["a=1,2,3", "b=4,5,6"]),
|
222
|
+
("a/k=1:3 b=4:6", ["a=1e3,2e3,3e3", "b=4,5,6"]),
|
223
|
+
("a/n=4,5 b=c,d", ["a=4e-9,5e-9", "b=c,d"]),
|
186
224
|
("", []),
|
187
225
|
],
|
188
226
|
)
|
189
|
-
def
|
227
|
+
def test_collect_str(s, x):
|
190
228
|
from hydraflow.executor.parser import collect
|
191
229
|
|
192
230
|
assert collect(s) == x
|
@@ -195,13 +233,14 @@ def test_collect(s, x):
|
|
195
233
|
@pytest.mark.parametrize(
|
196
234
|
("s", "x"),
|
197
235
|
[
|
198
|
-
(["a=1"], [["a=1"]]),
|
199
|
-
(["a=1,2"], [["a=
|
236
|
+
(["a=1", "b"], [["a=1"]]),
|
237
|
+
(["a/k=1,2"], [["a=1e3"], ["a=2e3"]]),
|
200
238
|
(
|
201
239
|
" a=1,2\n b=3,4\n",
|
202
240
|
[["a=1", "b=3"], ["a=1", "b=4"], ["a=2", "b=3"], ["a=2", "b=4"]],
|
203
241
|
),
|
204
242
|
(["a=1:2|3,4"], [["a=1,2"], ["a=3,4"]]),
|
243
|
+
(["a/k=1:2|3,4"], [["a=1e3,2e3"], ["a=3e3,4e3"]]),
|
205
244
|
(
|
206
245
|
["a=1:2|3,4", "b=5:6|c=7,8"],
|
207
246
|
[
|
@@ -211,10 +250,39 @@ def test_collect(s, x):
|
|
211
250
|
["a=3,4", "c=7,8"],
|
212
251
|
],
|
213
252
|
),
|
253
|
+
(
|
254
|
+
["a/m=1:2|3,4", "b/k=5:6|c/u=7,8"],
|
255
|
+
[
|
256
|
+
["a=1e-3,2e-3", "b=5e3,6e3"],
|
257
|
+
["a=1e-3,2e-3", "c=7e-6,8e-6"],
|
258
|
+
["a=3e-3,4e-3", "b=5e3,6e3"],
|
259
|
+
["a=3e-3,4e-3", "c=7e-6,8e-6"],
|
260
|
+
],
|
261
|
+
),
|
262
|
+
],
|
263
|
+
)
|
264
|
+
def test_expand_list(s, x):
|
265
|
+
from hydraflow.executor.parser import expand
|
266
|
+
|
267
|
+
assert expand(s) == x
|
268
|
+
|
269
|
+
|
270
|
+
@pytest.mark.parametrize(
|
271
|
+
("s", "x"),
|
272
|
+
[
|
273
|
+
(
|
274
|
+
"a/m=1:2|3,4 b/k=5:6|c=7,8",
|
275
|
+
[
|
276
|
+
["a=1e-3,2e-3", "b=5e3,6e3"],
|
277
|
+
["a=1e-3,2e-3", "c=7,8"],
|
278
|
+
["a=3e-3,4e-3", "b=5e3,6e3"],
|
279
|
+
["a=3e-3,4e-3", "c=7,8"],
|
280
|
+
],
|
281
|
+
),
|
214
282
|
("", [[]]),
|
215
283
|
],
|
216
284
|
)
|
217
|
-
def
|
285
|
+
def test_expand_str(s, x):
|
218
286
|
from hydraflow.executor.parser import expand
|
219
287
|
|
220
288
|
assert expand(s) == x
|
@@ -1,33 +0,0 @@
|
|
1
|
-
import sys
|
2
|
-
|
3
|
-
import pytest
|
4
|
-
from typer.testing import CliRunner
|
5
|
-
|
6
|
-
import hydraflow
|
7
|
-
from hydraflow.cli import app
|
8
|
-
|
9
|
-
pytestmark = pytest.mark.xdist_group(name="group1")
|
10
|
-
|
11
|
-
runner = CliRunner()
|
12
|
-
|
13
|
-
|
14
|
-
@pytest.mark.skipif(
|
15
|
-
sys.platform == "win32",
|
16
|
-
reason="Windows does not support this test",
|
17
|
-
)
|
18
|
-
def test_run_args():
|
19
|
-
result = runner.invoke(app, ["run", "args"])
|
20
|
-
assert result.exit_code == 0
|
21
|
-
run_ids = hydraflow.list_run_ids("args")
|
22
|
-
assert len(run_ids) == 12
|
23
|
-
|
24
|
-
|
25
|
-
@pytest.mark.skipif(
|
26
|
-
sys.platform == "win32",
|
27
|
-
reason="Windows does not support this test",
|
28
|
-
)
|
29
|
-
def test_run_batch():
|
30
|
-
result = runner.invoke(app, ["run", "batch"])
|
31
|
-
assert result.exit_code == 0
|
32
|
-
run_ids = hydraflow.list_run_ids("batch")
|
33
|
-
assert len(run_ids) == 8
|
@@ -1,23 +0,0 @@
|
|
1
|
-
from typer.testing import CliRunner
|
2
|
-
|
3
|
-
from hydraflow.cli import app
|
4
|
-
|
5
|
-
runner = CliRunner()
|
6
|
-
|
7
|
-
|
8
|
-
def test_show_args():
|
9
|
-
result = runner.invoke(app, ["show", "args"])
|
10
|
-
assert result.exit_code == 0
|
11
|
-
assert "hydra.job.name=args" in result.stdout
|
12
|
-
assert "count=1,2,3 name=a,b" in result.stdout
|
13
|
-
assert "count=4,5,6 name=c,d" in result.stdout
|
14
|
-
|
15
|
-
|
16
|
-
def test_show_batch():
|
17
|
-
result = runner.invoke(app, ["show", "batch"])
|
18
|
-
assert result.exit_code == 0
|
19
|
-
assert "hydra.job.name=batch" in result.stdout
|
20
|
-
assert "count=1,2 name=a" in result.stdout
|
21
|
-
assert "count=1,2 name=b" in result.stdout
|
22
|
-
assert "count=100 name=c,d" in result.stdout
|
23
|
-
assert "count=100 name=e,f" in result.stdout
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|