hydraflow 0.12.5__tar.gz → 0.13.0__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 (96) hide show
  1. {hydraflow-0.12.5 → hydraflow-0.13.0}/PKG-INFO +1 -1
  2. {hydraflow-0.12.5 → hydraflow-0.13.0}/pyproject.toml +2 -1
  3. hydraflow-0.13.0/src/hydraflow/cli.py +127 -0
  4. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/executor/conf.py +0 -1
  5. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/executor/job.py +83 -54
  6. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/cli/conftest.py +2 -0
  7. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/cli/hydraflow.yaml +7 -5
  8. hydraflow-0.13.0/tests/cli/submit.py +17 -0
  9. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/cli/test_run.py +37 -8
  10. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/executor/echo.py +2 -0
  11. hydraflow-0.13.0/tests/executor/read.py +15 -0
  12. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/executor/test_job.py +16 -21
  13. hydraflow-0.12.5/src/hydraflow/cli.py +0 -94
  14. {hydraflow-0.12.5 → hydraflow-0.13.0}/.devcontainer/devcontainer.json +0 -0
  15. {hydraflow-0.12.5 → hydraflow-0.13.0}/.devcontainer/postCreate.sh +0 -0
  16. {hydraflow-0.12.5 → hydraflow-0.13.0}/.devcontainer/starship.toml +0 -0
  17. {hydraflow-0.12.5 → hydraflow-0.13.0}/.gitattributes +0 -0
  18. {hydraflow-0.12.5 → hydraflow-0.13.0}/.github/workflows/ci.yaml +0 -0
  19. {hydraflow-0.12.5 → hydraflow-0.13.0}/.github/workflows/docs.yaml +0 -0
  20. {hydraflow-0.12.5 → hydraflow-0.13.0}/.github/workflows/publish.yaml +0 -0
  21. {hydraflow-0.12.5 → hydraflow-0.13.0}/.gitignore +0 -0
  22. {hydraflow-0.12.5 → hydraflow-0.13.0}/LICENSE +0 -0
  23. {hydraflow-0.12.5 → hydraflow-0.13.0}/README.md +0 -0
  24. {hydraflow-0.12.5 → hydraflow-0.13.0}/apps/quickstart.py +0 -0
  25. {hydraflow-0.12.5 → hydraflow-0.13.0}/docs/index.md +0 -0
  26. {hydraflow-0.12.5 → hydraflow-0.13.0}/docs/usage/quickstart.md +0 -0
  27. {hydraflow-0.12.5 → hydraflow-0.13.0}/mkdocs.yaml +0 -0
  28. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/__init__.py +0 -0
  29. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/core/__init__.py +0 -0
  30. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/core/config.py +0 -0
  31. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/core/context.py +0 -0
  32. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/core/io.py +0 -0
  33. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/core/main.py +0 -0
  34. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/core/mlflow.py +0 -0
  35. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/core/param.py +0 -0
  36. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/entities/__init__.py +0 -0
  37. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/entities/run_collection.py +0 -0
  38. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/entities/run_data.py +0 -0
  39. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/entities/run_info.py +0 -0
  40. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/executor/__init__.py +0 -0
  41. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/executor/io.py +0 -0
  42. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/executor/parser.py +0 -0
  43. {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/py.typed +0 -0
  44. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/__init__.py +0 -0
  45. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/cli/__init__.py +0 -0
  46. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/cli/app.py +0 -0
  47. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/cli/test_setup.py +0 -0
  48. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/cli/test_show.py +0 -0
  49. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/cli/test_version.py +0 -0
  50. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/conftest.py +0 -0
  51. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/__init__.py +0 -0
  52. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/config/__init__.py +0 -0
  53. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/config/test_config.py +0 -0
  54. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/config/test_params.py +0 -0
  55. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/context/__init__.py +0 -0
  56. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/context/chdir.py +0 -0
  57. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/context/log_run.py +0 -0
  58. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/context/start_run.py +0 -0
  59. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/context/test_chdir.py +0 -0
  60. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/context/test_log_run.py +0 -0
  61. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/context/test_start_run.py +0 -0
  62. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/io/__init__.py +0 -0
  63. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/io/hydra_dir.py +0 -0
  64. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/io/test_hydra_dir.py +0 -0
  65. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/io/test_iter_dirs.py +0 -0
  66. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/io/test_run.py +0 -0
  67. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/__init__.py +0 -0
  68. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/default.py +0 -0
  69. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/force_new_run.py +0 -0
  70. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/match_overrides.py +0 -0
  71. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/rerun_finished.py +0 -0
  72. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/skip_finished.py +0 -0
  73. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/test_default.py +0 -0
  74. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/test_force_new_run.py +0 -0
  75. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/test_match_overrides.py +0 -0
  76. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/test_rerun_finished.py +0 -0
  77. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/test_skip_finished.py +0 -0
  78. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/param/__init__.py +0 -0
  79. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/param/params.py +0 -0
  80. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/param/test_param.py +0 -0
  81. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/param/test_params.py +0 -0
  82. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/test_mlflow.py +0 -0
  83. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/entities/__init__.py +0 -0
  84. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/entities/filter.py +0 -0
  85. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/entities/test_collection.py +0 -0
  86. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/entities/test_data.py +0 -0
  87. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/entities/test_filter.py +0 -0
  88. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/entities/test_info.py +0 -0
  89. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/entities/test_values.py +0 -0
  90. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/entities/values.py +0 -0
  91. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/executor/__init__.py +0 -0
  92. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/executor/conftest.py +0 -0
  93. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/executor/test_args.py +0 -0
  94. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/executor/test_conf.py +0 -0
  95. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/executor/test_io.py +0 -0
  96. {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/executor/test_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hydraflow
3
- Version: 0.12.5
3
+ Version: 0.13.0
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.12.5"
7
+ version = "0.13.0"
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" }
@@ -92,6 +92,7 @@ ignore = [
92
92
  "S603",
93
93
  "SIM102",
94
94
  "SIM108",
95
+ "SIM115",
95
96
  "TRY003",
96
97
  ]
97
98
 
@@ -0,0 +1,127 @@
1
+ """Hydraflow CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shlex
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from typer import Argument, Option
11
+
12
+ app = typer.Typer(add_completion=False)
13
+ console = Console()
14
+
15
+
16
+ @app.command(context_settings={"ignore_unknown_options": True})
17
+ def run(
18
+ name: Annotated[str, Argument(help="Job name.", show_default=False)],
19
+ *,
20
+ args: Annotated[
21
+ list[str] | None,
22
+ Argument(help="Arguments to pass to the job.", show_default=False),
23
+ ] = None,
24
+ dry_run: Annotated[
25
+ bool,
26
+ Option("--dry-run", help="Perform a dry run."),
27
+ ] = False,
28
+ ) -> None:
29
+ """Run a job."""
30
+ from hydraflow.executor.io import get_job
31
+ from hydraflow.executor.job import iter_batches, iter_calls, iter_runs
32
+
33
+ args = args or []
34
+ job = get_job(name)
35
+
36
+ if job.run:
37
+ args = [*shlex.split(job.run), *args]
38
+ it = iter_runs(args, iter_batches(job), dry_run=dry_run)
39
+ elif job.call:
40
+ args = [*shlex.split(job.call), *args]
41
+ it = iter_calls(args, iter_batches(job), dry_run=dry_run)
42
+ else:
43
+ typer.echo(f"No command found in job: {job.name}.")
44
+ raise typer.Exit(1)
45
+
46
+ if not dry_run:
47
+ import mlflow
48
+
49
+ mlflow.set_experiment(job.name)
50
+
51
+ for task in it: # jobs will be executed here
52
+ if job.run and dry_run:
53
+ typer.echo(shlex.join(task.args))
54
+ elif job.call and dry_run:
55
+ funcname, *args = task.args
56
+ arg = ", ".join(f"{arg!r}" for arg in args)
57
+ typer.echo(f"{funcname}([{arg}])")
58
+
59
+
60
+ @app.command(context_settings={"ignore_unknown_options": True})
61
+ def submit(
62
+ name: Annotated[str, Argument(help="Job name.", show_default=False)],
63
+ *,
64
+ args: Annotated[
65
+ list[str] | None,
66
+ Argument(help="Arguments to pass to the job.", show_default=False),
67
+ ] = None,
68
+ dry_run: Annotated[
69
+ bool,
70
+ Option("--dry-run", help="Perform a dry run."),
71
+ ] = False,
72
+ ) -> None:
73
+ """Submit a job."""
74
+ from hydraflow.executor.io import get_job
75
+ from hydraflow.executor.job import iter_batches, submit
76
+
77
+ args = args or []
78
+ job = get_job(name)
79
+
80
+ if not job.run:
81
+ typer.echo(f"No run found in job: {job.name}.")
82
+ raise typer.Exit(1)
83
+
84
+ if not dry_run:
85
+ import mlflow
86
+
87
+ mlflow.set_experiment(job.name)
88
+
89
+ args = [*shlex.split(job.run), *args]
90
+ result = submit(args, iter_batches(job), dry_run=dry_run)
91
+
92
+ if dry_run and isinstance(result, tuple):
93
+ for line in result[1].splitlines():
94
+ args = shlex.split(line)
95
+ typer.echo(shlex.join([*result[0][:-1], *args]))
96
+
97
+
98
+ @app.command()
99
+ def show(
100
+ name: Annotated[str, Argument(help="Job name.", show_default=False)] = "",
101
+ ) -> None:
102
+ """Show the hydraflow config."""
103
+ from omegaconf import OmegaConf
104
+
105
+ from hydraflow.executor.io import get_job, load_config
106
+
107
+ if name:
108
+ cfg = get_job(name)
109
+ else:
110
+ cfg = load_config()
111
+
112
+ typer.echo(OmegaConf.to_yaml(cfg))
113
+
114
+
115
+ @app.callback(invoke_without_command=True)
116
+ def callback(
117
+ *,
118
+ version: Annotated[
119
+ bool,
120
+ Option("--version", help="Show the version and exit."),
121
+ ] = False,
122
+ ) -> None:
123
+ if version:
124
+ import importlib.metadata
125
+
126
+ typer.echo(f"hydraflow {importlib.metadata.version('hydraflow')}")
127
+ raise typer.Exit
@@ -15,7 +15,6 @@ class Job:
15
15
  name: str = ""
16
16
  run: str = ""
17
17
  call: str = ""
18
- submit: str = ""
19
18
  with_: str = ""
20
19
  steps: list[Step] = field(default_factory=list)
21
20
 
@@ -22,7 +22,10 @@ import shlex
22
22
  import subprocess
23
23
  import sys
24
24
  from dataclasses import dataclass
25
- from typing import TYPE_CHECKING
25
+ from pathlib import Path
26
+ from subprocess import CompletedProcess
27
+ from tempfile import NamedTemporaryFile
28
+ from typing import TYPE_CHECKING, overload
26
29
 
27
30
  import ulid
28
31
 
@@ -82,29 +85,49 @@ def iter_batches(job: Job) -> Iterator[list[str]]:
82
85
 
83
86
 
84
87
  @dataclass
85
- class Run:
86
- """An executed run."""
88
+ class Task:
89
+ """An executed task."""
87
90
 
91
+ args: list[str]
88
92
  total: int
89
93
  completed: int
94
+
95
+
96
+ @dataclass
97
+ class Run(Task):
98
+ """An executed run."""
99
+
90
100
  result: CompletedProcess
91
101
 
92
102
 
93
103
  @dataclass
94
- class Call:
104
+ class Call(Task):
95
105
  """An executed call."""
96
106
 
97
- total: int
98
- completed: int
99
107
  result: Any
100
108
 
101
109
 
110
+ @overload
111
+ def iter_runs(args: list[str], iterable: Iterable[list[str]]) -> Iterator[Run]: ...
112
+
113
+
114
+ @overload
115
+ def iter_runs(
116
+ args: list[str],
117
+ iterable: Iterable[list[str]],
118
+ *,
119
+ dry_run: bool = False,
120
+ ) -> Iterator[Task | Run]: ...
121
+
122
+
102
123
  def iter_runs(
103
- executable: str,
104
124
  args: list[str],
105
125
  iterable: Iterable[list[str]],
106
- ) -> Iterator[Run]:
126
+ *,
127
+ dry_run: bool = False,
128
+ ) -> Iterator[Task | Run]:
107
129
  """Execute multiple runs of a job using shell commands."""
130
+ executable, *args = args
108
131
  if executable == "python" and sys.platform == "win32":
109
132
  executable = sys.executable
110
133
 
@@ -112,34 +135,75 @@ def iter_runs(
112
135
  total = len(iterable)
113
136
 
114
137
  for completed, args_ in enumerate(iterable, 1):
115
- result = subprocess.run([executable, *args, *args_], check=False)
116
- yield Run(total, completed, result)
138
+ cmd = [executable, *args, *args_]
139
+ if dry_run:
140
+ yield Task(cmd, total, completed)
141
+ else:
142
+ result = subprocess.run(cmd, check=False)
143
+ yield Run(cmd, total, completed, result)
144
+
145
+
146
+ @overload
147
+ def iter_calls(args: list[str], iterable: Iterable[list[str]]) -> Iterator[Call]: ...
148
+
149
+
150
+ @overload
151
+ def iter_calls(
152
+ args: list[str],
153
+ iterable: Iterable[list[str]],
154
+ *,
155
+ dry_run: bool = False,
156
+ ) -> Iterator[Task | Call]: ...
117
157
 
118
158
 
119
159
  def iter_calls(
120
- funcname: str,
121
160
  args: list[str],
122
161
  iterable: Iterable[list[str]],
123
- ) -> Iterator[Call]:
162
+ *,
163
+ dry_run: bool = False,
164
+ ) -> Iterator[Task | Call]:
124
165
  """Execute multiple calls of a job using Python functions."""
166
+ funcname, *args = args
125
167
  func = get_callable(funcname)
126
168
 
127
169
  iterable = list(iterable)
128
170
  total = len(iterable)
129
171
 
130
172
  for completed, args_ in enumerate(iterable, 1):
131
- result = func([*args, *args_])
132
- yield Call(total, completed, result)
173
+ cmd = [funcname, *args, *args_]
174
+ if dry_run:
175
+ yield Task(cmd, total, completed)
176
+ else:
177
+ result = func([*args, *args_])
178
+ yield Call(cmd, total, completed, result)
133
179
 
134
180
 
135
181
  def submit(
136
- funcname: str,
137
182
  args: list[str],
138
183
  iterable: Iterable[list[str]],
139
- ) -> Any:
140
- """Submit entire job using Python functions."""
141
- func = get_callable(funcname)
142
- return func([[*args, *a] for a in iterable])
184
+ *,
185
+ dry_run: bool = False,
186
+ ) -> CompletedProcess | tuple[list[str], str]:
187
+ """Submit entire job using a shell command."""
188
+ executable, *args = args
189
+ if executable == "python" and sys.platform == "win32":
190
+ executable = sys.executable
191
+
192
+ temp = NamedTemporaryFile(dir=Path.cwd(), delete=False) # for Windows
193
+ file = Path(temp.name)
194
+ temp.close()
195
+
196
+ text = "\n".join(shlex.join(args) for args in iterable)
197
+ file.write_text(text)
198
+ cmd = [executable, *args, file.as_posix()]
199
+
200
+ try:
201
+ if dry_run:
202
+ return cmd, text
203
+ return subprocess.run(cmd, check=False)
204
+
205
+ finally:
206
+ file.unlink(missing_ok=True)
143
207
 
144
208
 
145
209
  def get_callable(name: str) -> Callable:
@@ -156,38 +220,3 @@ def get_callable(name: str) -> Callable:
156
220
  except (ImportError, AttributeError, ModuleNotFoundError) as e:
157
221
  msg = f"Failed to import or find function: {name}"
158
222
  raise ValueError(msg) from e
159
-
160
-
161
- def to_text(job: Job) -> str:
162
- """Convert the job configuration to a string.
163
-
164
- This function returns the job configuration for a given job.
165
-
166
- Args:
167
- job (Job): The job configuration to show.
168
-
169
- Returns:
170
- str: The job configuration.
171
-
172
- """
173
- text = ""
174
-
175
- it = iter_batches(job)
176
-
177
- if job.run:
178
- base_cmds = shlex.split(job.run)
179
- for args in it:
180
- cmds = " ".join([*base_cmds, *args])
181
- text += f"{cmds}\n"
182
-
183
- elif job.call:
184
- text = f"call: {job.call}\n"
185
- for args in it:
186
- text += f"args: {args}\n"
187
-
188
- elif job.submit:
189
- text = f"submit: {job.submit}\n"
190
- for args in it:
191
- text += f"args: {args}\n"
192
-
193
- return text.rstrip()
@@ -10,3 +10,5 @@ def setup(chdir):
10
10
  copy(src, src.name)
11
11
  src = Path(__file__).parent / "app.py"
12
12
  copy(src, src.name)
13
+ src = Path(__file__).parent / "submit.py"
14
+ copy(src, src.name)
@@ -28,12 +28,14 @@ jobs:
28
28
  - batch: name=c,d
29
29
  args: count=4:6
30
30
  submit:
31
- submit: typer.echo
31
+ run: python submit.py
32
32
  steps:
33
- - batch: name=a
34
- args: count=1,3
35
- - batch: name=b
36
- args: count=5,7
33
+ - batch: name=a,b
34
+ args: count=1
35
+ - batch: name=c
36
+ args: count=5
37
+ - batch: name=d
38
+ args: count=6
37
39
  error:
38
40
  steps:
39
41
  - batch: name=a
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ import shlex
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+
9
+ def main():
10
+ file = Path(sys.argv[-1])
11
+ for line in file.read_text().splitlines():
12
+ args = shlex.split(line)
13
+ subprocess.run([sys.executable, "app.py", *args], check=False)
14
+
15
+
16
+ if __name__ == "__main__":
17
+ main()
@@ -11,9 +11,9 @@ def test_run_args_dry_run():
11
11
  result = runner.invoke(app, ["run", "args", "--dry-run"])
12
12
  assert result.exit_code == 0
13
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
14
+ assert "app.py --multirun count=1,2,3 name=a,b" in out
15
+ assert "app.py --multirun count=4,5,6 name=c,d" in out
16
+ assert out.count("hydra.job.name=args") == 2
17
17
 
18
18
 
19
19
  def test_run_batch_dry_run():
@@ -24,7 +24,7 @@ def test_run_batch_dry_run():
24
24
  assert "name=b count=1,2" in out
25
25
  assert "name=c,d count=100" in out
26
26
  assert "name=e,f count=100" in out
27
- assert "hydra.job.name=batch" in out
27
+ assert out.count("hydra.job.name=batch") == 4
28
28
 
29
29
 
30
30
  def test_run_parallel_dry_run():
@@ -39,6 +39,27 @@ def test_run_parallel_dry_run():
39
39
  assert "hydra.launcher.n_jobs=4" in lines[1]
40
40
 
41
41
 
42
+ def test_run_parallel_dry_run_extra_args():
43
+ args = ["run", "parallel", "--dry-run", "a", "--b", "--", "--dry-run"]
44
+ result = runner.invoke(app, args)
45
+ assert result.exit_code == 0
46
+ assert result.stdout.count("app.py a --b --dry-run --multirun") == 2
47
+
48
+
49
+ def test_run_echo_dry_run():
50
+ args = ["run", "echo", "--dry-run"]
51
+ result = runner.invoke(app, args)
52
+ assert result.exit_code == 0
53
+ assert result.stdout.count("typer.echo(['a', 'b', 'c', '--multirun',") == 4
54
+
55
+
56
+ def test_submit_dry_run():
57
+ args = ["submit", "submit", "--dry-run", "a", "--b", "--", "--dry-run"]
58
+ result = runner.invoke(app, args)
59
+ assert result.exit_code == 0
60
+ assert result.stdout.count("submit.py a --b --dry-run --multirun") == 4
61
+
62
+
42
63
  @pytest.mark.xdist_group(name="group1")
43
64
  def test_run_args():
44
65
  result = runner.invoke(app, ["run", "args"])
@@ -77,13 +98,14 @@ def test_run_echo():
77
98
 
78
99
 
79
100
  @pytest.mark.xdist_group(name="group4")
80
- def test_run_submit():
81
- result = runner.invoke(app, ["run", "submit"])
101
+ def test_submit():
102
+ result = runner.invoke(app, ["submit", "submit"])
82
103
  assert result.exit_code == 0
83
104
  out = result.stdout
84
105
  lines = out.splitlines()
85
- assert len(lines) == 2
86
- assert "[['--multirun', 'name=a', 'count=1,3', 'hydra.job.name=submit'" in lines[1]
106
+ assert len(lines) == 1
107
+ run_ids = hydraflow.list_run_ids("submit")
108
+ assert len(run_ids) == 4
87
109
 
88
110
 
89
111
  @pytest.mark.xdist_group(name="group5")
@@ -91,3 +113,10 @@ def test_run_error():
91
113
  result = runner.invoke(app, ["run", "error"])
92
114
  assert result.exit_code == 1
93
115
  assert "No command found in job: error." in result.stdout
116
+
117
+
118
+ @pytest.mark.xdist_group(name="group5")
119
+ def test_submit_error():
120
+ result = runner.invoke(app, ["submit", "error"])
121
+ assert result.exit_code == 1
122
+ assert "No run found in job: error." in result.stdout
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import sys
2
4
  from pathlib import Path
3
5
 
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+
7
+ def main():
8
+ path = Path(sys.argv[1])
9
+ src = Path(sys.argv[2])
10
+ text = src.read_text()
11
+ path.write_text(text)
12
+
13
+
14
+ if __name__ == "__main__":
15
+ main()
@@ -65,8 +65,8 @@ def test_iter_runs(job: Job, tmp_path: Path):
65
65
  path = tmp_path / "output.txt"
66
66
  file = Path(__file__).parent / "echo.py"
67
67
 
68
- args = [file.as_posix(), path.as_posix()]
69
- x = list(iter_runs("python", args, iter_batches(job)))
68
+ args = ["python", file.as_posix(), path.as_posix()]
69
+ x = list(iter_runs(args, iter_batches(job)))
70
70
  assert path.read_text() == "b=5 a=1,2 b=6 a=1,2 c=7 a=3,4 c=8 a=3,4"
71
71
  assert x[0].completed == 1
72
72
  assert x[0].result.returncode == 0
@@ -79,7 +79,7 @@ def test_iter_runs(job: Job, tmp_path: Path):
79
79
  def test_iter_calls(job: Job, capsys: pytest.CaptureFixture):
80
80
  from hydraflow.executor.job import iter_batches, iter_calls
81
81
 
82
- x = list(iter_calls("typer.echo", [], iter_batches(job)))
82
+ x = list(iter_calls(["typer.echo"], iter_batches(job)))
83
83
  out, _ = capsys.readouterr()
84
84
  assert "'b=5', 'a=1,2'" in out
85
85
  assert "'c=8', 'a=3,4'" in out
@@ -92,20 +92,25 @@ def test_iter_calls_args(job: Job, capsys: pytest.CaptureFixture):
92
92
  from hydraflow.executor.job import iter_batches, iter_calls
93
93
 
94
94
  job.call = "typer.echo a 'b c'"
95
- list(iter_calls("typer.echo", ["a", "b c"], iter_batches(job)))
95
+ list(iter_calls(["typer.echo", "a", "b c"], iter_batches(job)))
96
96
  out, _ = capsys.readouterr()
97
97
  assert "['a', 'b c', '--multirun'," in out
98
98
 
99
99
 
100
- def test_submit(job: Job, capsys: pytest.CaptureFixture):
100
+ def test_submit(job: Job, tmp_path: Path):
101
101
  from hydraflow.executor.job import iter_batches, submit
102
102
 
103
- submit("typer.echo", ["a"], iter_batches(job))
104
- out, _ = capsys.readouterr()
105
- assert out.startswith("[['a', '--multirun', 'b=5', 'a=1,2', 'hydra.job.name=test'")
106
- assert "], ['a', '--multirun', 'b=6', 'a=1,2', 'hydra" in out
107
- assert "], ['a', '--multirun', 'c=7', 'a=3,4', 'hydra" in out
108
- assert "], ['a', '--multirun', 'c=8', 'a=3,4', 'hydra" in out
103
+ path = tmp_path / "output.txt"
104
+ file = Path(__file__).parent / "read.py"
105
+
106
+ args = ["python", file.as_posix(), path.as_posix()]
107
+ submit(args, iter_batches(job))
108
+ lines = path.read_text().splitlines()
109
+ assert len(lines) == 4
110
+ assert lines[0].startswith("--multirun b=5 a=1,2 ")
111
+ assert lines[1].startswith("--multirun b=6 a=1,2 ")
112
+ assert lines[2].startswith("--multirun c=7 a=3,4 ")
113
+ assert lines[3].startswith("--multirun c=8 a=3,4 ")
109
114
 
110
115
 
111
116
  def test_get_callable_error():
@@ -120,13 +125,3 @@ def test_get_callable_not_found():
120
125
 
121
126
  with pytest.raises(ValueError):
122
127
  get_callable("hydraflow.invalid")
123
-
124
-
125
- def test_to_text(job: Job):
126
- from hydraflow.executor.job import to_text
127
-
128
- job.call = "typer.echo"
129
- text = to_text(job)
130
- assert "call: typer.echo\n" 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
@@ -1,94 +0,0 @@
1
- """Hydraflow CLI."""
2
-
3
- from __future__ import annotations
4
-
5
- import shlex
6
- from typing import Annotated
7
-
8
- import typer
9
- from rich.console import Console
10
- from typer import Argument, Option
11
-
12
- app = typer.Typer(add_completion=False)
13
- console = Console()
14
-
15
-
16
- @app.command()
17
- def run(
18
- name: Annotated[str, Argument(help="Job name.", show_default=False)],
19
- *,
20
- dry_run: Annotated[
21
- bool,
22
- Option("--dry-run", help="Perform a dry run"),
23
- ] = False,
24
- ) -> None:
25
- """Run a job."""
26
-
27
- from hydraflow.executor.io import get_job
28
- from hydraflow.executor.job import (
29
- iter_batches,
30
- iter_calls,
31
- iter_runs,
32
- submit,
33
- to_text,
34
- )
35
-
36
- job = get_job(name)
37
-
38
- if dry_run:
39
- typer.echo(to_text(job))
40
- raise typer.Exit
41
-
42
- import mlflow
43
-
44
- mlflow.set_experiment(job.name)
45
-
46
- if job.submit:
47
- funcname, *args = shlex.split(job.submit)
48
- submit(funcname, args, iter_batches(job))
49
- raise typer.Exit
50
-
51
- if job.run:
52
- executable, *args = shlex.split(job.run)
53
- it = iter_runs(executable, args, iter_batches(job))
54
- elif job.call:
55
- funcname, *args = shlex.split(job.call)
56
- it = iter_calls(funcname, args, iter_batches(job))
57
- else:
58
- typer.echo(f"No command found in job: {job.name}.")
59
- raise typer.Exit(1)
60
-
61
- for _ in it:
62
- pass
63
-
64
-
65
- @app.command()
66
- def show(
67
- name: Annotated[str, Argument(help="Job name.", show_default=False)] = "",
68
- ) -> None:
69
- """Show the hydraflow config."""
70
- from omegaconf import OmegaConf
71
-
72
- from hydraflow.executor.io import get_job, load_config
73
-
74
- if name:
75
- cfg = get_job(name)
76
- else:
77
- cfg = load_config()
78
-
79
- typer.echo(OmegaConf.to_yaml(cfg))
80
-
81
-
82
- @app.callback(invoke_without_command=True)
83
- def callback(
84
- *,
85
- version: Annotated[
86
- bool,
87
- Option("--version", help="Show the version and exit."),
88
- ] = False,
89
- ) -> None:
90
- if version:
91
- import importlib.metadata
92
-
93
- typer.echo(f"hydraflow {importlib.metadata.version('hydraflow')}")
94
- raise typer.Exit
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