hydraflow 0.14.0__tar.gz → 0.14.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.
Files changed (97) hide show
  1. {hydraflow-0.14.0 → hydraflow-0.14.2}/PKG-INFO +2 -1
  2. {hydraflow-0.14.0 → hydraflow-0.14.2}/pyproject.toml +2 -1
  3. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/cli.py +44 -26
  4. hydraflow-0.14.2/src/hydraflow/executor/aio.py +85 -0
  5. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/executor/job.py +13 -65
  6. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/cli/test_run.py +18 -1
  7. hydraflow-0.14.2/tests/executor/test_aio.py +22 -0
  8. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/executor/test_job.py +10 -14
  9. {hydraflow-0.14.0 → hydraflow-0.14.2}/.devcontainer/devcontainer.json +0 -0
  10. {hydraflow-0.14.0 → hydraflow-0.14.2}/.devcontainer/postCreate.sh +0 -0
  11. {hydraflow-0.14.0 → hydraflow-0.14.2}/.devcontainer/starship.toml +0 -0
  12. {hydraflow-0.14.0 → hydraflow-0.14.2}/.gitattributes +0 -0
  13. {hydraflow-0.14.0 → hydraflow-0.14.2}/.github/workflows/ci.yaml +0 -0
  14. {hydraflow-0.14.0 → hydraflow-0.14.2}/.github/workflows/docs.yaml +0 -0
  15. {hydraflow-0.14.0 → hydraflow-0.14.2}/.github/workflows/publish.yaml +0 -0
  16. {hydraflow-0.14.0 → hydraflow-0.14.2}/.gitignore +0 -0
  17. {hydraflow-0.14.0 → hydraflow-0.14.2}/LICENSE +0 -0
  18. {hydraflow-0.14.0 → hydraflow-0.14.2}/README.md +0 -0
  19. {hydraflow-0.14.0 → hydraflow-0.14.2}/apps/quickstart.py +0 -0
  20. {hydraflow-0.14.0 → hydraflow-0.14.2}/docs/index.md +0 -0
  21. {hydraflow-0.14.0 → hydraflow-0.14.2}/docs/usage/quickstart.md +0 -0
  22. {hydraflow-0.14.0 → hydraflow-0.14.2}/mkdocs.yaml +0 -0
  23. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/__init__.py +0 -0
  24. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/core/__init__.py +0 -0
  25. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/core/config.py +0 -0
  26. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/core/context.py +0 -0
  27. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/core/io.py +0 -0
  28. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/core/main.py +0 -0
  29. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/core/mlflow.py +0 -0
  30. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/core/param.py +0 -0
  31. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/entities/__init__.py +0 -0
  32. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/entities/run_collection.py +0 -0
  33. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/entities/run_data.py +0 -0
  34. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/entities/run_info.py +0 -0
  35. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/executor/__init__.py +0 -0
  36. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/executor/conf.py +0 -0
  37. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/executor/io.py +0 -0
  38. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/executor/parser.py +0 -0
  39. {hydraflow-0.14.0 → hydraflow-0.14.2}/src/hydraflow/py.typed +0 -0
  40. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/__init__.py +0 -0
  41. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/cli/__init__.py +0 -0
  42. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/cli/app.py +0 -0
  43. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/cli/conftest.py +0 -0
  44. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/cli/hydraflow.yaml +0 -0
  45. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/cli/submit.py +0 -0
  46. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/cli/test_setup.py +0 -0
  47. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/cli/test_show.py +0 -0
  48. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/cli/test_version.py +0 -0
  49. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/conftest.py +0 -0
  50. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/__init__.py +0 -0
  51. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/config/__init__.py +0 -0
  52. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/config/test_config.py +0 -0
  53. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/config/test_params.py +0 -0
  54. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/context/__init__.py +0 -0
  55. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/context/chdir.py +0 -0
  56. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/context/log_run.py +0 -0
  57. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/context/start_run.py +0 -0
  58. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/context/test_chdir.py +0 -0
  59. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/context/test_log_run.py +0 -0
  60. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/context/test_start_run.py +0 -0
  61. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/io/__init__.py +0 -0
  62. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/io/hydra_dir.py +0 -0
  63. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/io/test_hydra_dir.py +0 -0
  64. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/io/test_iter_dirs.py +0 -0
  65. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/io/test_run.py +0 -0
  66. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/main/__init__.py +0 -0
  67. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/main/default.py +0 -0
  68. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/main/force_new_run.py +0 -0
  69. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/main/match_overrides.py +0 -0
  70. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/main/rerun_finished.py +0 -0
  71. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/main/skip_finished.py +0 -0
  72. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/main/test_default.py +0 -0
  73. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/main/test_force_new_run.py +0 -0
  74. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/main/test_match_overrides.py +0 -0
  75. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/main/test_rerun_finished.py +0 -0
  76. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/main/test_skip_finished.py +0 -0
  77. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/param/__init__.py +0 -0
  78. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/param/params.py +0 -0
  79. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/param/test_param.py +0 -0
  80. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/param/test_params.py +0 -0
  81. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/core/test_mlflow.py +0 -0
  82. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/entities/__init__.py +0 -0
  83. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/entities/filter.py +0 -0
  84. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/entities/test_collection.py +0 -0
  85. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/entities/test_data.py +0 -0
  86. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/entities/test_filter.py +0 -0
  87. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/entities/test_info.py +0 -0
  88. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/entities/test_values.py +0 -0
  89. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/entities/values.py +0 -0
  90. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/executor/__init__.py +0 -0
  91. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/executor/conftest.py +0 -0
  92. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/executor/echo.py +0 -0
  93. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/executor/read.py +0 -0
  94. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/executor/test_args.py +0 -0
  95. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/executor/test_conf.py +0 -0
  96. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/executor/test_io.py +0 -0
  97. {hydraflow-0.14.0 → hydraflow-0.14.2}/tests/executor/test_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hydraflow
3
- Version: 0.14.0
3
+ Version: 0.14.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
@@ -40,6 +40,7 @@ Requires-Dist: hydra-core>=1.3
40
40
  Requires-Dist: mlflow>=2.15
41
41
  Requires-Dist: omegaconf>=2.3
42
42
  Requires-Dist: python-ulid>=3.0.0
43
+ Requires-Dist: rich>=13.9
43
44
  Requires-Dist: typer>=0.15
44
45
  Description-Content-Type: text/markdown
45
46
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hydraflow"
7
- version = "0.14.0"
7
+ version = "0.14.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" }
@@ -24,6 +24,7 @@ dependencies = [
24
24
  "mlflow>=2.15",
25
25
  "omegaconf>=2.3",
26
26
  "python-ulid>=3.0.0",
27
+ "rich>=13.9",
27
28
  "typer>=0.15",
28
29
  ]
29
30
 
@@ -14,8 +14,8 @@ if TYPE_CHECKING:
14
14
  app = typer.Typer(add_completion=False)
15
15
 
16
16
 
17
- @app.command(context_settings={"ignore_unknown_options": True})
18
- def run(
17
+ @app.command("run", context_settings={"ignore_unknown_options": True})
18
+ def _run(
19
19
  name: Annotated[str, Argument(help="Job name.", show_default=False)],
20
20
  *,
21
21
  args: Annotated[
@@ -29,55 +29,73 @@ def run(
29
29
  ) -> None:
30
30
  """Run a job."""
31
31
  from hydraflow.executor.io import get_job
32
- from hydraflow.executor.job import iter_batches, iter_calls, iter_runs
33
32
 
34
33
  args = args or []
35
34
  job = get_job(name)
36
35
 
36
+ if not dry_run:
37
+ import mlflow
38
+
39
+ mlflow.set_experiment(job.name)
40
+
37
41
  if job.submit:
38
42
  submit(job, args, dry_run=dry_run)
39
- raise Exit
40
43
 
41
- if job.run:
42
- args = [*shlex.split(job.run), *args]
43
- it = iter_runs(args, iter_batches(job), dry_run=dry_run)
44
+ elif job.run:
45
+ run(job, args, dry_run=dry_run)
46
+
44
47
  elif job.call:
45
- args = [*shlex.split(job.call), *args]
46
- it = iter_calls(args, iter_batches(job), dry_run=dry_run)
48
+ call(job, args, dry_run=dry_run)
49
+
47
50
  else:
48
51
  typer.echo(f"No command found in job: {job.name}.")
49
52
  raise Exit(1)
50
53
 
54
+
55
+ def run(job: Job, args: list[str], *, dry_run: bool) -> None:
56
+ """Run a job."""
57
+ from hydraflow.executor import aio
58
+ from hydraflow.executor.job import iter_batches, iter_tasks
59
+
60
+ args = [*shlex.split(job.run), *args]
61
+ it = iter_tasks(args, iter_batches(job))
62
+
51
63
  if not dry_run:
52
- import mlflow
64
+ aio.run(it)
65
+ raise Exit
53
66
 
54
- mlflow.set_experiment(job.name)
67
+ for task in it:
68
+ typer.echo(shlex.join(task.args))
55
69
 
56
- for task in it: # jobs will be executed here
57
- if job.run and dry_run:
58
- typer.echo(shlex.join(task.args))
59
- elif job.call and dry_run:
60
- funcname, *args = task.args
61
- arg = ", ".join(f"{arg!r}" for arg in args)
62
- typer.echo(f"{funcname}([{arg}])")
63
70
 
71
+ def call(job: Job, args: list[str], *, dry_run: bool) -> None:
72
+ """Call a job."""
73
+ from hydraflow.executor.job import iter_batches, iter_calls
64
74
 
65
- def submit(job: Job, args: list[str], *, dry_run: bool) -> None:
66
- """Submit a job."""
67
- from hydraflow.executor.job import iter_batches, submit
75
+ args = [*shlex.split(job.call), *args]
76
+ it = iter_calls(args, iter_batches(job))
68
77
 
69
78
  if not dry_run:
70
- import mlflow
79
+ for call in it:
80
+ call.func()
81
+ raise Exit
71
82
 
72
- mlflow.set_experiment(job.name)
83
+ for task in it:
84
+ funcname, *args = task.args
85
+ arg = ", ".join(f"{arg!r}" for arg in args)
86
+ typer.echo(f"{funcname}([{arg}])")
87
+
88
+
89
+ def submit(job: Job, args: list[str], *, dry_run: bool) -> None:
90
+ """Submit a job."""
91
+ from hydraflow.executor.job import iter_batches, submit
73
92
 
74
93
  args = [*shlex.split(job.submit), *args]
75
94
  result = submit(args, iter_batches(job), dry_run=dry_run)
76
95
 
77
96
  if dry_run and isinstance(result, tuple):
78
- for line in result[1].splitlines():
79
- args = shlex.split(line)
80
- typer.echo(shlex.join([*result[0][:-1], *args]))
97
+ typer.echo(shlex.join(result[0]))
98
+ typer.echo(result[1])
81
99
 
82
100
 
83
101
  @app.command()
@@ -0,0 +1,85 @@
1
+ """Asynchronous execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from asyncio.subprocess import PIPE
7
+ from typing import TYPE_CHECKING
8
+
9
+ from rich.console import Console
10
+ from rich.progress import (
11
+ BarColumn,
12
+ MofNCompleteColumn,
13
+ Progress,
14
+ SpinnerColumn,
15
+ TaskProgressColumn,
16
+ TimeElapsedColumn,
17
+ TimeRemainingColumn,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from asyncio.streams import StreamReader
22
+ from collections.abc import Callable, Iterable
23
+
24
+ from hydraflow.executor.job import Task
25
+
26
+
27
+ console = Console(log_time=False, log_path=False)
28
+
29
+
30
+ def run(iterable: Iterable[Task]) -> int | None:
31
+ """Run multiple tasks."""
32
+ with Progress(
33
+ SpinnerColumn(),
34
+ TimeElapsedColumn(),
35
+ BarColumn(),
36
+ TimeRemainingColumn(),
37
+ MofNCompleteColumn(),
38
+ TaskProgressColumn(),
39
+ console=console,
40
+ ) as progress:
41
+
42
+ def stdout(output: str) -> None:
43
+ progress.log(output.rstrip())
44
+
45
+ def stderr(output: str) -> None:
46
+ progress.log(f"[red]{output}".rstrip())
47
+
48
+ task_id = progress.add_task("")
49
+
50
+ for task in iterable:
51
+ progress.update(task_id, total=task.total)
52
+
53
+ coro = arun(task.args, stdout, stderr)
54
+ returncode = asyncio.run(coro)
55
+
56
+ if returncode:
57
+ return returncode
58
+
59
+ progress.update(task_id, completed=task.index + 1)
60
+
61
+ return 0
62
+
63
+
64
+ async def arun(
65
+ args: list[str],
66
+ stdout: Callable[[str], None],
67
+ stderr: Callable[[str], None],
68
+ ) -> int | None:
69
+ """Run a command asynchronously."""
70
+ process = await asyncio.create_subprocess_exec(*args, stdout=PIPE, stderr=PIPE)
71
+ coros = alog(process.stdout, stdout), alog(process.stderr, stderr) # type:ignore
72
+ await asyncio.gather(*coros)
73
+ await process.communicate()
74
+
75
+ return process.returncode
76
+
77
+
78
+ async def alog(reader: StreamReader, write: Callable[[str], None]) -> None:
79
+ """Log a stream of output asynchronously."""
80
+ while True:
81
+ if reader.at_eof():
82
+ break
83
+
84
+ if out := await reader.readline():
85
+ write(out.decode())
@@ -25,7 +25,7 @@ from dataclasses import dataclass
25
25
  from pathlib import Path
26
26
  from subprocess import CompletedProcess
27
27
  from tempfile import NamedTemporaryFile
28
- from typing import TYPE_CHECKING, overload
28
+ from typing import TYPE_CHECKING
29
29
 
30
30
  import ulid
31
31
 
@@ -86,47 +86,22 @@ def iter_batches(job: Job) -> Iterator[list[str]]:
86
86
 
87
87
  @dataclass
88
88
  class Task:
89
- """An executed task."""
89
+ """A task to be executed."""
90
90
 
91
91
  args: list[str]
92
92
  total: int
93
- completed: int
94
-
95
-
96
- @dataclass
97
- class Run(Task):
98
- """An executed run."""
99
-
100
- result: CompletedProcess
93
+ index: int
101
94
 
102
95
 
103
96
  @dataclass
104
97
  class Call(Task):
105
- """An executed call."""
98
+ """A call to be executed."""
106
99
 
107
- result: Any
100
+ func: Callable[[], Any]
108
101
 
109
102
 
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
-
123
- def iter_runs(
124
- args: list[str],
125
- iterable: Iterable[list[str]],
126
- *,
127
- dry_run: bool = False,
128
- ) -> Iterator[Task | Run]:
129
- """Execute multiple runs of a job using shell commands."""
103
+ def iter_tasks(args: list[str], iterable: Iterable[list[str]]) -> Iterator[Task]:
104
+ """Yield tasks of a job to be executed using a shell command."""
130
105
  executable, *args = args
131
106
  if executable == "python" and sys.platform == "win32":
132
107
  executable = sys.executable
@@ -134,48 +109,21 @@ def iter_runs(
134
109
  iterable = list(iterable)
135
110
  total = len(iterable)
136
111
 
137
- for completed, args_ in enumerate(iterable, 1):
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]: ...
112
+ for index, args_ in enumerate(iterable):
113
+ yield Task([executable, *args, *args_], total, index)
148
114
 
149
115
 
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]: ...
157
-
158
-
159
- def iter_calls(
160
- args: list[str],
161
- iterable: Iterable[list[str]],
162
- *,
163
- dry_run: bool = False,
164
- ) -> Iterator[Task | Call]:
165
- """Execute multiple calls of a job using Python functions."""
116
+ def iter_calls(args: list[str], iterable: Iterable[list[str]]) -> Iterator[Call]:
117
+ """Yield calls of a job to be executed using a Python function."""
166
118
  funcname, *args = args
167
119
  func = get_callable(funcname)
168
120
 
169
121
  iterable = list(iterable)
170
122
  total = len(iterable)
171
123
 
172
- for completed, args_ in enumerate(iterable, 1):
124
+ for index, args_ in enumerate(iterable):
173
125
  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)
126
+ yield Call(cmd, total, index, lambda x=cmd[1:]: func(x))
179
127
 
180
128
 
181
129
  def submit(
@@ -57,7 +57,14 @@ def test_submit_dry_run():
57
57
  args = ["run", "submit", "--dry-run", "a", "--b", "--", "--dry-run"]
58
58
  result = runner.invoke(app, args)
59
59
  assert result.exit_code == 0
60
- assert result.stdout.count("submit.py a --b --dry-run --multirun") == 4
60
+ assert result.stdout.count("submit.py a --b --dry-run") == 1
61
+ assert result.stdout.count("--multirun") == 4
62
+ lines = result.stdout.splitlines()
63
+ assert len(lines) == 5
64
+ assert "name=a count=1" in lines[1]
65
+ assert "name=b count=1" in lines[2]
66
+ assert "name=c count=5" in lines[3]
67
+ assert "name=d count=6" in lines[4]
61
68
 
62
69
 
63
70
  @pytest.mark.xdist_group(name="group1")
@@ -83,6 +90,11 @@ def test_run_parallel():
83
90
  run_ids = hydraflow.list_run_ids("parallel")
84
91
  assert len(run_ids) == 8
85
92
 
93
+ result = runner.invoke(app, ["run", "parallel"]) # skip if already run
94
+ assert result.exit_code == 0
95
+ run_ids = hydraflow.list_run_ids("parallel")
96
+ assert len(run_ids) == 8
97
+
86
98
 
87
99
  @pytest.mark.xdist_group(name="group4")
88
100
  def test_run_echo():
@@ -107,6 +119,11 @@ def test_submit():
107
119
  run_ids = hydraflow.list_run_ids("submit")
108
120
  assert len(run_ids) == 4
109
121
 
122
+ result = runner.invoke(app, ["run", "submit"]) # skip if already run
123
+ assert result.exit_code == 0
124
+ run_ids = hydraflow.list_run_ids("submit")
125
+ assert len(run_ids) == 4
126
+
110
127
 
111
128
  @pytest.mark.xdist_group(name="group5")
112
129
  def test_run_error():
@@ -0,0 +1,22 @@
1
+ import sys
2
+
3
+ import pytest
4
+
5
+
6
+ @pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows")
7
+ def test_run_returncode():
8
+ from hydraflow.executor.aio import run
9
+ from hydraflow.executor.job import Task
10
+
11
+ task = Task(args=["false"], index=0, total=1)
12
+
13
+ assert run([task]) == 1
14
+
15
+
16
+ def test_run_stderr():
17
+ from hydraflow.executor.aio import run
18
+ from hydraflow.executor.job import Task
19
+
20
+ task = Task(args=["python", "-c", "1/0"], index=0, total=1)
21
+
22
+ assert run([task]) == 1
@@ -59,40 +59,36 @@ def test_sweep_args(batches, i, x):
59
59
  assert batches[i][-3] == x
60
60
 
61
61
 
62
- def test_iter_runs(job: Job, tmp_path: Path):
63
- from hydraflow.executor.job import iter_batches, iter_runs
62
+ def test_iter_tasks(job: Job, tmp_path: Path):
63
+ import subprocess
64
+
65
+ from hydraflow.executor.job import iter_batches, iter_tasks
64
66
 
65
67
  path = tmp_path / "output.txt"
66
68
  file = Path(__file__).parent / "echo.py"
67
69
 
68
70
  args = ["python", file.as_posix(), path.as_posix()]
69
- x = list(iter_runs(args, iter_batches(job)))
71
+ for task in iter_tasks(args, iter_batches(job)):
72
+ subprocess.run(task.args, check=True)
70
73
  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
- assert x[0].completed == 1
72
- assert x[0].result.returncode == 0
73
- assert x[1].completed == 2
74
- assert x[1].result.returncode == 0
75
- assert x[2].completed == 3
76
- assert x[2].result.returncode == 0
77
74
 
78
75
 
79
76
  def test_iter_calls(job: Job, capsys: pytest.CaptureFixture):
80
77
  from hydraflow.executor.job import iter_batches, iter_calls
81
78
 
82
- x = list(iter_calls(["typer.echo"], iter_batches(job)))
79
+ for call in iter_calls(["typer.echo"], iter_batches(job)):
80
+ call.func()
83
81
  out, _ = capsys.readouterr()
84
82
  assert "'b=5', 'a=1,2'" in out
85
83
  assert "'c=8', 'a=3,4'" in out
86
- assert x[0].completed == 1
87
- assert x[1].completed == 2
88
- assert x[2].completed == 3
89
84
 
90
85
 
91
86
  def test_iter_calls_args(job: Job, capsys: pytest.CaptureFixture):
92
87
  from hydraflow.executor.job import iter_batches, iter_calls
93
88
 
94
89
  job.call = "typer.echo a 'b c'"
95
- list(iter_calls(["typer.echo", "a", "b c"], iter_batches(job)))
90
+ for call in iter_calls(["typer.echo", "a", "b c"], iter_batches(job)):
91
+ call.func()
96
92
  out, _ = capsys.readouterr()
97
93
  assert "['a', 'b c', '--multirun'," in out
98
94
 
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