hydraflow 0.10.2__tar.gz → 0.11.1__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.
Files changed (95) hide show
  1. {hydraflow-0.10.2 → hydraflow-0.11.1}/PKG-INFO +1 -2
  2. {hydraflow-0.10.2 → hydraflow-0.11.1}/README.md +0 -1
  3. {hydraflow-0.10.2 → hydraflow-0.11.1}/pyproject.toml +2 -1
  4. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/executor/conf.py +3 -2
  5. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/executor/job.py +14 -11
  6. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/executor/parser.py +68 -6
  7. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/cli/app.py +7 -1
  8. hydraflow-0.11.1/tests/cli/hydraflow.yaml +22 -0
  9. hydraflow-0.11.1/tests/cli/test_run.py +63 -0
  10. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/executor/echo.py +1 -1
  11. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/executor/test_conf.py +2 -2
  12. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/executor/test_job.py +20 -30
  13. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/executor/test_parser.py +16 -2
  14. hydraflow-0.10.2/tests/cli/hydraflow.yaml +0 -13
  15. hydraflow-0.10.2/tests/cli/test_run.py +0 -41
  16. {hydraflow-0.10.2 → hydraflow-0.11.1}/.cursorrules +0 -0
  17. {hydraflow-0.10.2 → hydraflow-0.11.1}/.devcontainer/devcontainer.json +0 -0
  18. {hydraflow-0.10.2 → hydraflow-0.11.1}/.devcontainer/postCreate.sh +0 -0
  19. {hydraflow-0.10.2 → hydraflow-0.11.1}/.devcontainer/starship.toml +0 -0
  20. {hydraflow-0.10.2 → hydraflow-0.11.1}/.gitattributes +0 -0
  21. {hydraflow-0.10.2 → hydraflow-0.11.1}/.github/workflows/ci.yaml +0 -0
  22. {hydraflow-0.10.2 → hydraflow-0.11.1}/.github/workflows/docs.yaml +0 -0
  23. {hydraflow-0.10.2 → hydraflow-0.11.1}/.gitignore +0 -0
  24. {hydraflow-0.10.2 → hydraflow-0.11.1}/LICENSE +0 -0
  25. {hydraflow-0.10.2 → hydraflow-0.11.1}/apps/quickstart.py +0 -0
  26. {hydraflow-0.10.2 → hydraflow-0.11.1}/docs/index.md +0 -0
  27. {hydraflow-0.10.2 → hydraflow-0.11.1}/docs/usage/quickstart.md +0 -0
  28. {hydraflow-0.10.2 → hydraflow-0.11.1}/mkdocs.yaml +0 -0
  29. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/__init__.py +0 -0
  30. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/cli.py +0 -0
  31. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/core/__init__.py +0 -0
  32. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/core/config.py +0 -0
  33. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/core/context.py +0 -0
  34. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/core/io.py +0 -0
  35. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/core/main.py +0 -0
  36. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/core/mlflow.py +0 -0
  37. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/core/param.py +0 -0
  38. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/entities/__init__.py +0 -0
  39. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/entities/run_collection.py +0 -0
  40. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/entities/run_data.py +0 -0
  41. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/entities/run_info.py +0 -0
  42. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/executor/__init__.py +0 -0
  43. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/executor/io.py +0 -0
  44. {hydraflow-0.10.2 → hydraflow-0.11.1}/src/hydraflow/py.typed +0 -0
  45. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/__init__.py +0 -0
  46. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/cli/__init__.py +0 -0
  47. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/cli/conftest.py +0 -0
  48. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/cli/test_setup.py +0 -0
  49. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/cli/test_show.py +0 -0
  50. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/cli/test_version.py +0 -0
  51. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/conftest.py +0 -0
  52. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/__init__.py +0 -0
  53. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/config/__init__.py +0 -0
  54. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/config/test_config.py +0 -0
  55. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/config/test_params.py +0 -0
  56. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/context/__init__.py +0 -0
  57. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/context/chdir.py +0 -0
  58. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/context/log_run.py +0 -0
  59. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/context/start_run.py +0 -0
  60. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/context/test_chdir.py +0 -0
  61. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/context/test_log_run.py +0 -0
  62. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/context/test_start_run.py +0 -0
  63. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/io/__init__.py +0 -0
  64. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/io/hydra_dir.py +0 -0
  65. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/io/test_hydra_dir.py +0 -0
  66. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/io/test_iter_dirs.py +0 -0
  67. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/io/test_run.py +0 -0
  68. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/main/__init__.py +0 -0
  69. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/main/default.py +0 -0
  70. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/main/force_new_run.py +0 -0
  71. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/main/match_overrides.py +0 -0
  72. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/main/rerun_finished.py +0 -0
  73. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/main/skip_finished.py +0 -0
  74. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/main/test_default.py +0 -0
  75. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/main/test_force_new_run.py +0 -0
  76. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/main/test_match_overrides.py +0 -0
  77. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/main/test_rerun_finished.py +0 -0
  78. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/main/test_skip_finished.py +0 -0
  79. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/param/__init__.py +0 -0
  80. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/param/params.py +0 -0
  81. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/param/test_param.py +0 -0
  82. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/param/test_params.py +0 -0
  83. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/core/test_mlflow.py +0 -0
  84. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/entities/__init__.py +0 -0
  85. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/entities/filter.py +0 -0
  86. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/entities/test_collection.py +0 -0
  87. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/entities/test_data.py +0 -0
  88. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/entities/test_filter.py +0 -0
  89. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/entities/test_info.py +0 -0
  90. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/entities/test_values.py +0 -0
  91. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/entities/values.py +0 -0
  92. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/executor/__init__.py +0 -0
  93. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/executor/conftest.py +0 -0
  94. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/executor/test_args.py +0 -0
  95. {hydraflow-0.10.2 → hydraflow-0.11.1}/tests/executor/test_io.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hydraflow
3
- Version: 0.10.2
3
+ Version: 0.11.1
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
@@ -97,7 +97,6 @@ Here is a simple example to get you started with Hydraflow:
97
97
  from __future__ import annotations
98
98
 
99
99
  from dataclasses import dataclass
100
- from pathlib import Path
101
100
  from typing import TYPE_CHECKING
102
101
 
103
102
  import hydraflow
@@ -51,7 +51,6 @@ Here is a simple example to get you started with Hydraflow:
51
51
  from __future__ import annotations
52
52
 
53
53
  from dataclasses import dataclass
54
- from pathlib import Path
55
54
  from typing import TYPE_CHECKING
56
55
 
57
56
  import hydraflow
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hydraflow"
7
- version = "0.10.2"
7
+ version = "0.11.1"
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" }
@@ -38,6 +38,7 @@ hydraflow = "hydraflow.cli:app"
38
38
 
39
39
  [dependency-groups]
40
40
  dev = [
41
+ "hydra-joblib-launcher",
41
42
  "pytest-clarity",
42
43
  "pytest-cov",
43
44
  "pytest-order",
@@ -5,9 +5,9 @@ from dataclasses import dataclass, field
5
5
 
6
6
  @dataclass
7
7
  class Step:
8
- args: str = ""
9
8
  batch: str = ""
10
- options: str = ""
9
+ args: str = ""
10
+ configs: str = ""
11
11
 
12
12
 
13
13
  @dataclass
@@ -15,6 +15,7 @@ class Job:
15
15
  name: str = ""
16
16
  run: str = ""
17
17
  call: str = ""
18
+ configs: str = ""
18
19
  steps: list[Step] = field(default_factory=list)
19
20
 
20
21
 
@@ -12,7 +12,7 @@ The module supports two execution modes:
12
12
  2. Python function calls
13
13
 
14
14
  Each job can consist of multiple steps, and each step can have its own
15
- arguments and options that will be expanded into multiple runs.
15
+ arguments and configurations that will be expanded into multiple runs.
16
16
  """
17
17
 
18
18
  from __future__ import annotations
@@ -31,27 +31,27 @@ from .parser import collect, expand
31
31
  if TYPE_CHECKING:
32
32
  from collections.abc import Iterator
33
33
 
34
- from .conf import Job, Step
34
+ from .conf import Job
35
35
 
36
36
 
37
- def iter_args(step: Step) -> Iterator[list[str]]:
37
+ def iter_args(batch: str, args: str) -> Iterator[list[str]]:
38
38
  """Iterate over combinations generated from parsed arguments.
39
39
 
40
40
  Generate all possible combinations of arguments by parsing and
41
41
  expanding each one, yielding them as an iterator.
42
42
 
43
43
  Args:
44
- step (Step): The step to parse.
44
+ batch (str): The batch to parse.
45
+ args (str): The arguments to parse.
45
46
 
46
47
  Yields:
47
48
  list[str]: a list of the parsed argument combinations.
48
49
 
49
50
  """
50
- args = collect(step.args)
51
- options = [o for o in step.options.split(" ") if o]
51
+ args_ = collect(args)
52
52
 
53
- for batch in expand(step.batch):
54
- yield [*options, *sorted([*batch, *args])]
53
+ for batch_ in expand(batch):
54
+ yield [*batch_, *args_]
55
55
 
56
56
 
57
57
  def iter_batches(job: Job) -> Iterator[list[str]]:
@@ -69,11 +69,14 @@ def iter_batches(job: Job) -> Iterator[list[str]]:
69
69
 
70
70
  """
71
71
  job_name = f"hydra.job.name={job.name}"
72
+ job_configs = shlex.split(job.configs)
72
73
 
73
74
  for step in job.steps:
74
- for args in iter_args(step):
75
+ configs = shlex.split(step.configs) or job_configs
76
+
77
+ for args in iter_args(step.batch, step.args):
75
78
  sweep_dir = f"hydra.sweep.dir=multirun/{ulid.ULID()}"
76
- yield ["--multirun", sweep_dir, job_name, *args]
79
+ yield ["--multirun", *args, job_name, sweep_dir, *configs]
77
80
 
78
81
 
79
82
  def multirun(job: Job) -> None:
@@ -162,4 +165,4 @@ def to_text(job: Job) -> str:
162
165
  for args in it:
163
166
  text += f"args: {args}\n"
164
167
 
165
- return text
168
+ return text.rstrip()
@@ -216,6 +216,9 @@ def collect_values(arg: str) -> list[str]:
216
216
  list[str]: A list of the collected values.
217
217
 
218
218
  """
219
+ if "(" in arg:
220
+ return collect_parentheses(arg)
221
+
219
222
  if ":" not in arg:
220
223
  return [arg]
221
224
 
@@ -235,6 +238,58 @@ def collect_values(arg: str) -> list[str]:
235
238
  return [add_exponent(x, exponent) for x in values]
236
239
 
237
240
 
241
+ def split_parentheses(arg: str) -> Iterator[str]:
242
+ """Split a string with parentheses into a list of strings.
243
+
244
+ Args:
245
+ arg (str): The string to split.
246
+
247
+ Returns:
248
+ Iterator[str]: An iterator of the split strings.
249
+
250
+ Examples:
251
+ >>> list(split_parentheses("a(b,c)m(e:f)k"))
252
+ ['a', 'b,c', 'e-3', 'e:f', 'e3']
253
+ >>> list(split_parentheses("(b,c)d(e:f)"))
254
+ ['b,c', 'd', 'e:f']
255
+
256
+ """
257
+ current = ""
258
+
259
+ for char in arg:
260
+ if char in ("(", ")"):
261
+ if current:
262
+ yield SUFFIX_EXPONENT.get(current, current)
263
+ current = ""
264
+ else:
265
+ current += char
266
+
267
+ if current:
268
+ yield SUFFIX_EXPONENT.get(current, current)
269
+
270
+
271
+ def collect_parentheses(arg: str) -> list[str]:
272
+ """Collect values from a string with parentheses.
273
+
274
+ Args:
275
+ arg (str): The string to collect values from.
276
+
277
+ Returns:
278
+ list[str]: A list of the collected values.
279
+
280
+ Examples:
281
+ >>> collect_parentheses("(1:3,5:2:9,20)k")
282
+ ['1e3', '2e3', '3e3', '5e3', '7e3', '9e3', '20e3']
283
+ >>> collect_parentheses("2e(-1,-2,-3)")
284
+ ['2e-1', '2e-2', '2e-3']
285
+ >>> collect_parentheses("(1:3)e(3,5)")
286
+ ['1e3', '2e3', '3e3', '1e5', '2e5', '3e5']
287
+
288
+ """
289
+ it = [expand_values(x) for x in split_parentheses(arg)]
290
+ return ["".join(x[::-1]) for x in product(*it[::-1])]
291
+
292
+
238
293
  def split(arg: str) -> list[str]:
239
294
  r"""Split a string by top-level commas.
240
295
 
@@ -255,11 +310,14 @@ def split(arg: str) -> list[str]:
255
310
  ['"x,y"', 'z']
256
311
  >>> split("'p,q',r")
257
312
  ["'p,q'", 'r']
313
+ >>> split("(a,b)m,(1,2:4)k")
314
+ ['(a,b)m', '(1,2:4)k']
258
315
 
259
316
  """
260
317
  result = []
261
318
  current = []
262
319
  bracket_count = 0
320
+ paren_count = 0
263
321
  in_single_quote = False
264
322
  in_double_quote = False
265
323
 
@@ -272,9 +330,14 @@ def split(arg: str) -> list[str]:
272
330
  bracket_count += 1
273
331
  elif char == "]" and not (in_single_quote or in_double_quote):
274
332
  bracket_count -= 1
333
+ elif char == "(" and not (in_single_quote or in_double_quote):
334
+ paren_count += 1
335
+ elif char == ")" and not (in_single_quote or in_double_quote):
336
+ paren_count -= 1
275
337
  elif (
276
338
  char == ","
277
339
  and bracket_count == 0
340
+ and paren_count == 0
278
341
  and not in_single_quote
279
342
  and not in_double_quote
280
343
  ):
@@ -303,8 +366,7 @@ def expand_values(arg: str, suffix: str = "") -> Iterator[str]:
303
366
  Iterator[str]: An iterator of the expanded values.
304
367
 
305
368
  """
306
- if suffix in SUFFIX_EXPONENT:
307
- suffix = SUFFIX_EXPONENT[suffix]
369
+ suffix = SUFFIX_EXPONENT.get(suffix, suffix)
308
370
 
309
371
  for value in chain.from_iterable(collect_values(x) for x in split(arg)):
310
372
  yield f"{value}{suffix}"
@@ -320,6 +382,10 @@ def split_arg(arg: str) -> tuple[str, str, str]:
320
382
  tuple[str, str, str]: A tuple containing the key, suffix, and value.
321
383
 
322
384
  """
385
+ if "=" not in arg:
386
+ msg = f"Invalid argument: {arg}"
387
+ raise ValueError(msg)
388
+
323
389
  key, value = arg.split("=")
324
390
 
325
391
  if "/" in key:
@@ -399,8 +465,6 @@ def collect(args: str | list[str]) -> list[str]:
399
465
  if isinstance(args, str):
400
466
  args = shlex.split(args)
401
467
 
402
- args = [arg for arg in args if "=" in arg]
403
-
404
468
  return [collect_arg(arg) for arg in args]
405
469
 
406
470
 
@@ -417,6 +481,4 @@ def expand(args: str | list[str]) -> list[list[str]]:
417
481
  if isinstance(args, str):
418
482
  args = shlex.split(args)
419
483
 
420
- args = [arg for arg in args if "=" in arg]
421
-
422
484
  return [list(x) for x in product(*(expand_arg(arg) for arg in args))]
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
4
+ import time
3
5
  from dataclasses import dataclass
4
6
  from typing import TYPE_CHECKING
5
7
 
@@ -8,6 +10,8 @@ import hydraflow
8
10
  if TYPE_CHECKING:
9
11
  from mlflow.entities import Run
10
12
 
13
+ log = logging.getLogger(__name__)
14
+
11
15
 
12
16
  @dataclass
13
17
  class Config:
@@ -17,7 +21,9 @@ class Config:
17
21
 
18
22
  @hydraflow.main(Config)
19
23
  def app(run: Run, cfg: Config):
20
- pass
24
+ log.info("start")
25
+ time.sleep(0.2)
26
+ log.info("end")
21
27
 
22
28
 
23
29
  if __name__ == "__main__":
@@ -0,0 +1,22 @@
1
+ jobs:
2
+ args:
3
+ run: python app.py
4
+ steps:
5
+ - args: count=1:3 name=a,b
6
+ - args: count=4:6 name=c,d
7
+ batch:
8
+ run: python app.py
9
+ steps:
10
+ - batch: name=a,b
11
+ args: count=1,2
12
+ - batch: name=c,d|e,f
13
+ args: count=100
14
+ parallel:
15
+ run: python app.py
16
+ configs: hydra/launcher=joblib hydra.launcher.n_jobs=2
17
+ steps:
18
+ - batch: name=a
19
+ args: count=1:4
20
+ - batch: name=b
21
+ args: count=11:14
22
+ configs: hydra/launcher=joblib hydra.launcher.n_jobs=4
@@ -0,0 +1,63 @@
1
+ import pytest
2
+ from typer.testing import CliRunner
3
+
4
+ import hydraflow
5
+ from hydraflow.cli import app
6
+
7
+ runner = CliRunner()
8
+
9
+
10
+ def test_run_args_dry_run():
11
+ result = runner.invoke(app, ["run", "args", "--dry-run"])
12
+ assert result.exit_code == 0
13
+ out = result.stdout
14
+ assert "hydra.job.name=args" in out
15
+ assert "count=1,2,3 name=a,b" in out
16
+ assert "count=4,5,6 name=c,d" in out
17
+
18
+
19
+ def test_run_batch_dry_run():
20
+ result = runner.invoke(app, ["run", "batch", "--dry-run"])
21
+ assert result.exit_code == 0
22
+ out = result.stdout
23
+ assert "name=a count=1,2" in out
24
+ assert "name=b count=1,2" in out
25
+ assert "name=c,d count=100" in out
26
+ assert "name=e,f count=100" in out
27
+ assert "hydra.job.name=batch" in out
28
+
29
+
30
+ def test_run_parallel_dry_run():
31
+ result = runner.invoke(app, ["run", "parallel", "--dry-run"])
32
+ assert result.exit_code == 0
33
+ out = result.stdout
34
+ lines = out.splitlines()
35
+ assert len(lines) == 2
36
+ assert "count=1,2,3,4" in lines[0]
37
+ assert "hydra.launcher.n_jobs=2" in lines[0]
38
+ assert "count=11,12,13,14" in lines[1]
39
+ assert "hydra.launcher.n_jobs=4" in lines[1]
40
+
41
+
42
+ @pytest.mark.xdist_group(name="group1")
43
+ def test_run_args():
44
+ result = runner.invoke(app, ["run", "args"])
45
+ assert result.exit_code == 0
46
+ run_ids = hydraflow.list_run_ids("args")
47
+ assert len(run_ids) == 12
48
+
49
+
50
+ @pytest.mark.xdist_group(name="group2")
51
+ def test_run_batch():
52
+ result = runner.invoke(app, ["run", "batch"])
53
+ assert result.exit_code == 0
54
+ run_ids = hydraflow.list_run_ids("batch")
55
+ assert len(run_ids) == 8
56
+
57
+
58
+ @pytest.mark.xdist_group(name="group3")
59
+ def test_run_parallel():
60
+ result = runner.invoke(app, ["run", "parallel"])
61
+ assert result.exit_code == 0
62
+ run_ids = hydraflow.list_run_ids("parallel")
63
+ assert len(run_ids) == 8
@@ -4,7 +4,7 @@ from pathlib import Path
4
4
 
5
5
  def main():
6
6
  path = Path(sys.argv[1])
7
- arg = " ".join(sys.argv[-2:])
7
+ arg = " ".join(sys.argv[3:5])
8
8
 
9
9
  if not path.exists():
10
10
  path.write_text(arg)
@@ -30,5 +30,5 @@ def test_job(config):
30
30
 
31
31
 
32
32
  def test_step(config):
33
- cfg = config("jobs:\n a:\n steps:\n - options: --opt1 --opt2\n")
34
- assert cfg.jobs["a"].steps[0].options == "--opt1 --opt2"
33
+ cfg = config("jobs:\n a:\n steps:\n - configs: --opt1 --opt2\n")
34
+ assert cfg.jobs["a"].steps[0].configs == "--opt1 --opt2"
@@ -9,36 +9,25 @@ from hydraflow.executor.conf import Job, Step
9
9
  def test_iter_args():
10
10
  from hydraflow.executor.job import iter_args
11
11
 
12
- step = Step(args="a=1:3", batch="b=3,4 c=5,6")
13
- it = iter_args(step)
14
- assert next(it) == ["a=1,2,3", "b=3", "c=5"]
15
- assert next(it) == ["a=1,2,3", "b=3", "c=6"]
16
- assert next(it) == ["a=1,2,3", "b=4", "c=5"]
17
- assert next(it) == ["a=1,2,3", "b=4", "c=6"]
12
+ it = iter_args("b=3,4 c=5,6", "a=1:3")
13
+ assert next(it) == ["b=3", "c=5", "a=1,2,3"]
14
+ assert next(it) == ["b=3", "c=6", "a=1,2,3"]
15
+ assert next(it) == ["b=4", "c=5", "a=1,2,3"]
16
+ assert next(it) == ["b=4", "c=6", "a=1,2,3"]
18
17
 
19
18
 
20
19
  def test_iter_args_pipe():
21
20
  from hydraflow.executor.job import iter_args
22
21
 
23
- step = Step(args="a=1:3", batch="b=3,4|c=5:7")
24
- it = iter_args(step)
25
- assert next(it) == ["a=1,2,3", "b=3,4"]
26
- assert next(it) == ["a=1,2,3", "c=5,6,7"]
27
-
28
-
29
- def test_iter_args_with_options():
30
- from hydraflow.executor.job import iter_args
31
-
32
- step = Step(args="a=1:3", batch="b=3,4", options="--opt1 --opt2")
33
- it = iter_args(step)
34
- assert next(it) == ["--opt1", "--opt2", "a=1,2,3", "b=3"]
35
- assert next(it) == ["--opt1", "--opt2", "a=1,2,3", "b=4"]
22
+ it = iter_args("b=3,4|c=5:7", "a=1:3")
23
+ assert next(it) == ["b=3,4", "a=1,2,3"]
24
+ assert next(it) == ["c=5,6,7", "a=1,2,3"]
36
25
 
37
26
 
38
27
  @pytest.fixture
39
28
  def job():
40
- s1 = Step(args="a=1:2", batch="b=5,6")
41
- s2 = Step(args="a=3:4", batch="c=7,8")
29
+ s1 = Step(batch="b=5,6", args="a=1:2")
30
+ s2 = Step(batch="c=7,8", args="a=3:4")
42
31
  return Job(name="test", steps=[s1, s2])
43
32
 
44
33
 
@@ -50,17 +39,17 @@ def batches(job: Job):
50
39
 
51
40
 
52
41
  def test_sweep_dir(batches):
53
- assert all(x[1].startswith("hydra.sweep.dir=multirun/") for x in batches)
54
- assert all(len(x[1].split("/")[-1]) == 26 for x in batches)
42
+ assert all(x[-1].startswith("hydra.sweep.dir=multirun/") for x in batches)
43
+ assert all(len(x[-1].split("/")[-1]) == 26 for x in batches)
55
44
 
56
45
 
57
46
  def test_job_name(batches):
58
- assert all(x[2].startswith("hydra.job.name=test") for x in batches)
47
+ assert all(x[-2].startswith("hydra.job.name=test") for x in batches)
59
48
 
60
49
 
61
50
  @pytest.mark.parametrize(("i", "x"), [(0, "b=5"), (1, "b=6"), (2, "c=7"), (3, "c=8")])
62
51
  def test_batch_args(batches, i, x):
63
- assert batches[i][-1] == x
52
+ assert batches[i][1] == x
64
53
 
65
54
 
66
55
  @pytest.mark.parametrize(
@@ -68,7 +57,7 @@ def test_batch_args(batches, i, x):
68
57
  [(0, "a=1,2"), (1, "a=1,2"), (2, "a=3,4"), (3, "a=3,4")],
69
58
  )
70
59
  def test_sweep_args(batches, i, x):
71
- assert batches[i][-2] == x
60
+ assert batches[i][-3] == x
72
61
 
73
62
 
74
63
  def test_multirun_run(job: Job, tmp_path: Path):
@@ -79,7 +68,7 @@ def test_multirun_run(job: Job, tmp_path: Path):
79
68
 
80
69
  job.run = f"python {file.as_posix()} {path.as_posix()}"
81
70
  multirun(job)
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"
71
+ assert path.read_text() == "b=5 a=1,2 b=6 a=1,2 c=7 a=3,4 c=8 a=3,4"
83
72
 
84
73
 
85
74
  def test_multirun_run_error(job: Job):
@@ -96,8 +85,8 @@ def test_multirun_call(job: Job, capsys: pytest.CaptureFixture):
96
85
  job.call = "typer.echo"
97
86
  multirun(job)
98
87
  out, _ = capsys.readouterr()
99
- assert "'a=1,2', 'b=5'" in out
100
- assert "'a=3,4', 'c=8'" in out
88
+ assert "'b=5', 'a=1,2'" in out
89
+ assert "'c=8', 'a=3,4'" in out
101
90
 
102
91
 
103
92
  def test_multirun_call_args(job: Job, capsys: pytest.CaptureFixture):
@@ -139,4 +128,5 @@ def test_to_text(job: Job):
139
128
  job.call = "typer.echo"
140
129
  text = to_text(job)
141
130
  assert "call: typer.echo\n" in text
142
- assert "'hydra.job.name=test', 'a=3,4', 'c=8']" in text
131
+ assert "'b=5', 'a=1,2', 'hydra.job.name=test'" in text
132
+ assert "'c=8', 'a=3,4', 'hydra.job.name=test'" in text
@@ -103,6 +103,7 @@ def test_split_suffix(s, x):
103
103
  ("1:2:e2", ["1e2", "2e2"]),
104
104
  (":2:e2", ["0", "1e2", "2e2"]),
105
105
  ("-2:2:k", ["-2e3", "-1e3", "0", "1e3", "2e3"]),
106
+ ("(1:3,5:2:9,20)k", ["1e3", "2e3", "3e3", "5e3", "7e3", "9e3", "20e3"]),
106
107
  ],
107
108
  )
108
109
  def test_collect_value(s, x):
@@ -124,6 +125,8 @@ def test_collect_value(s, x):
124
125
  ("[1,2],[3,4]", ["[1,2]", "[3,4]"]),
125
126
  ("'1,2','3,4'", ["'1,2'", "'3,4'"]),
126
127
  ('"1,2","3,4"', ['"1,2"', '"3,4"']),
128
+ ("(1,4)k,(6,8)M", ["1e3", "4e3", "6e6", "8e6"]),
129
+ ("(1:3)e-2,(5:7)e-3", ["1e-2", "2e-2", "3e-2", "5e-3", "6e-3", "7e-3"]),
127
130
  ],
128
131
  )
129
132
  def test_expand_value(s, x):
@@ -147,6 +150,13 @@ def test_expand_value_suffix(s, x):
147
150
  assert list(expand_values(s, "k")) == x
148
151
 
149
152
 
153
+ def test_split_arg_error():
154
+ from hydraflow.executor.parser import split_arg
155
+
156
+ with pytest.raises(ValueError):
157
+ split_arg("1,2,3")
158
+
159
+
150
160
  @pytest.mark.parametrize(
151
161
  ("s", "x"),
152
162
  [
@@ -157,6 +167,7 @@ def test_expand_value_suffix(s, x):
157
167
  ("a=1:2", "a=1,2"),
158
168
  ("a/M=1:2", "a=1e6,2e6"),
159
169
  ("a=:2:3", "a=0,2"),
170
+ ("a=(2,4)m,2,3", "a=2e-3,4e-3,2,3"),
160
171
  ("a=1:3:k", "a=1e3,2e3,3e3"),
161
172
  ("a=1:3:k,2:4:M", "a=1e3,2e3,3e3,2e6,3e6,4e6"),
162
173
  ("a/m=1:3,8:10", "a=1e-3,2e-3,3e-3,8e-3,9e-3,10e-3"),
@@ -177,6 +188,9 @@ def test_collect_arg(s, x):
177
188
  ("a=1:2", ["a=1", "a=2"]),
178
189
  ("a/n=1:2", ["a=1e-9", "a=2e-9"]),
179
190
  ("a=:2:3", ["a=0", "a=2"]),
191
+ ("a=(0.1:0.1:0.4)k", ["a=0.1e3", "a=0.2e3", "a=0.3e3", "a=0.4e3"]),
192
+ ("a=(1,2)(e-1,e2)", ["a=1e-1", "a=2e-1", "a=1e2", "a=2e2"]),
193
+ ("a=(1,2)e(-1,2)", ["a=1e-1", "a=2e-1", "a=1e2", "a=2e2"]),
180
194
  ("a=1:3:k", ["a=1e3", "a=2e3", "a=3e3"]),
181
195
  ("a=1:3:k,2:4:M", ["a=1e3", "a=2e3", "a=3e3", "a=2e6", "a=3e6", "a=4e6"]),
182
196
  ("a=1,2|3,4", ["a=1,2", "a=3,4"]),
@@ -202,7 +216,7 @@ def test_expand_arg_error():
202
216
  @pytest.mark.parametrize(
203
217
  ("s", "x"),
204
218
  [
205
- (["a=1", "b"], ["a=1"]),
219
+ (["a=1"], ["a=1"]),
206
220
  (["a=1:3"], ["a=1,2,3"]),
207
221
  (["a/m=1:3"], ["a=1e-3,2e-3,3e-3"]),
208
222
  (["a=1:3", "b=4:6"], ["a=1,2,3", "b=4,5,6"]),
@@ -233,7 +247,7 @@ def test_collect_str(s, x):
233
247
  @pytest.mark.parametrize(
234
248
  ("s", "x"),
235
249
  [
236
- (["a=1", "b"], [["a=1"]]),
250
+ (["a=1"], [["a=1"]]),
237
251
  (["a/k=1,2"], [["a=1e3"], ["a=2e3"]]),
238
252
  (
239
253
  " a=1,2\n b=3,4\n",
@@ -1,13 +0,0 @@
1
- jobs:
2
- args:
3
- run: python app.py
4
- steps:
5
- - args: count=1:3 name=a,b
6
- - args: count=4:6 name=c,d
7
- batch:
8
- run: python app.py
9
- steps:
10
- - args: count=1,2
11
- batch: name=a,b
12
- - args: count=100
13
- batch: name=c,d|e,f
@@ -1,41 +0,0 @@
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes