hydraflow 0.13.1__tar.gz → 0.14.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 (97) hide show
  1. {hydraflow-0.13.1 → hydraflow-0.14.1}/PKG-INFO +2 -1
  2. {hydraflow-0.13.1 → hydraflow-0.14.1}/pyproject.toml +2 -1
  3. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/cli.py +49 -43
  4. hydraflow-0.14.1/src/hydraflow/executor/aio.py +85 -0
  5. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/executor/conf.py +1 -0
  6. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/executor/job.py +13 -65
  7. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/hydraflow.yaml +1 -1
  8. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/test_run.py +12 -9
  9. hydraflow-0.14.1/tests/executor/test_aio.py +22 -0
  10. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/test_job.py +10 -14
  11. {hydraflow-0.13.1 → hydraflow-0.14.1}/.devcontainer/devcontainer.json +0 -0
  12. {hydraflow-0.13.1 → hydraflow-0.14.1}/.devcontainer/postCreate.sh +0 -0
  13. {hydraflow-0.13.1 → hydraflow-0.14.1}/.devcontainer/starship.toml +0 -0
  14. {hydraflow-0.13.1 → hydraflow-0.14.1}/.gitattributes +0 -0
  15. {hydraflow-0.13.1 → hydraflow-0.14.1}/.github/workflows/ci.yaml +0 -0
  16. {hydraflow-0.13.1 → hydraflow-0.14.1}/.github/workflows/docs.yaml +0 -0
  17. {hydraflow-0.13.1 → hydraflow-0.14.1}/.github/workflows/publish.yaml +0 -0
  18. {hydraflow-0.13.1 → hydraflow-0.14.1}/.gitignore +0 -0
  19. {hydraflow-0.13.1 → hydraflow-0.14.1}/LICENSE +0 -0
  20. {hydraflow-0.13.1 → hydraflow-0.14.1}/README.md +0 -0
  21. {hydraflow-0.13.1 → hydraflow-0.14.1}/apps/quickstart.py +0 -0
  22. {hydraflow-0.13.1 → hydraflow-0.14.1}/docs/index.md +0 -0
  23. {hydraflow-0.13.1 → hydraflow-0.14.1}/docs/usage/quickstart.md +0 -0
  24. {hydraflow-0.13.1 → hydraflow-0.14.1}/mkdocs.yaml +0 -0
  25. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/__init__.py +0 -0
  26. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/core/__init__.py +0 -0
  27. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/core/config.py +0 -0
  28. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/core/context.py +0 -0
  29. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/core/io.py +0 -0
  30. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/core/main.py +0 -0
  31. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/core/mlflow.py +0 -0
  32. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/core/param.py +0 -0
  33. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/entities/__init__.py +0 -0
  34. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/entities/run_collection.py +0 -0
  35. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/entities/run_data.py +0 -0
  36. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/entities/run_info.py +0 -0
  37. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/executor/__init__.py +0 -0
  38. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/executor/io.py +0 -0
  39. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/executor/parser.py +0 -0
  40. {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/py.typed +0 -0
  41. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/__init__.py +0 -0
  42. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/__init__.py +0 -0
  43. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/app.py +0 -0
  44. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/conftest.py +0 -0
  45. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/submit.py +0 -0
  46. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/test_setup.py +0 -0
  47. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/test_show.py +0 -0
  48. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/test_version.py +0 -0
  49. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/conftest.py +0 -0
  50. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/__init__.py +0 -0
  51. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/config/__init__.py +0 -0
  52. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/config/test_config.py +0 -0
  53. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/config/test_params.py +0 -0
  54. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/context/__init__.py +0 -0
  55. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/context/chdir.py +0 -0
  56. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/context/log_run.py +0 -0
  57. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/context/start_run.py +0 -0
  58. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/context/test_chdir.py +0 -0
  59. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/context/test_log_run.py +0 -0
  60. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/context/test_start_run.py +0 -0
  61. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/io/__init__.py +0 -0
  62. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/io/hydra_dir.py +0 -0
  63. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/io/test_hydra_dir.py +0 -0
  64. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/io/test_iter_dirs.py +0 -0
  65. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/io/test_run.py +0 -0
  66. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/__init__.py +0 -0
  67. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/default.py +0 -0
  68. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/force_new_run.py +0 -0
  69. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/match_overrides.py +0 -0
  70. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/rerun_finished.py +0 -0
  71. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/skip_finished.py +0 -0
  72. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/test_default.py +0 -0
  73. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/test_force_new_run.py +0 -0
  74. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/test_match_overrides.py +0 -0
  75. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/test_rerun_finished.py +0 -0
  76. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/test_skip_finished.py +0 -0
  77. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/param/__init__.py +0 -0
  78. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/param/params.py +0 -0
  79. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/param/test_param.py +0 -0
  80. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/param/test_params.py +0 -0
  81. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/test_mlflow.py +0 -0
  82. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/entities/__init__.py +0 -0
  83. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/entities/filter.py +0 -0
  84. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/entities/test_collection.py +0 -0
  85. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/entities/test_data.py +0 -0
  86. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/entities/test_filter.py +0 -0
  87. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/entities/test_info.py +0 -0
  88. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/entities/test_values.py +0 -0
  89. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/entities/values.py +0 -0
  90. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/__init__.py +0 -0
  91. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/conftest.py +0 -0
  92. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/echo.py +0 -0
  93. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/read.py +0 -0
  94. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/test_args.py +0 -0
  95. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/test_conf.py +0 -0
  96. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/test_io.py +0 -0
  97. {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/test_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hydraflow
3
- Version: 0.13.1
3
+ Version: 0.14.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
@@ -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.13.1"
7
+ version = "0.14.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" }
@@ -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
 
@@ -3,16 +3,19 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import shlex
6
- from typing import Annotated
6
+ from typing import TYPE_CHECKING, Annotated
7
7
 
8
8
  import typer
9
9
  from typer import Argument, Exit, Option
10
10
 
11
+ if TYPE_CHECKING:
12
+ from hydraflow.executor.conf import Job
13
+
11
14
  app = typer.Typer(add_completion=False)
12
15
 
13
16
 
14
- @app.command(context_settings={"ignore_unknown_options": True})
15
- def run(
17
+ @app.command("run", context_settings={"ignore_unknown_options": True})
18
+ def _run(
16
19
  name: Annotated[str, Argument(help="Job name.", show_default=False)],
17
20
  *,
18
21
  args: Annotated[
@@ -26,65 +29,68 @@ def run(
26
29
  ) -> None:
27
30
  """Run a job."""
28
31
  from hydraflow.executor.io import get_job
29
- from hydraflow.executor.job import iter_batches, iter_calls, iter_runs
30
32
 
31
33
  args = args or []
32
34
  job = get_job(name)
33
35
 
34
- if job.run:
35
- args = [*shlex.split(job.run), *args]
36
- it = iter_runs(args, iter_batches(job), dry_run=dry_run)
36
+ if not dry_run:
37
+ import mlflow
38
+
39
+ mlflow.set_experiment(job.name)
40
+
41
+ if job.submit:
42
+ submit(job, args, dry_run=dry_run)
43
+
44
+ elif job.run:
45
+ run(job, args, dry_run=dry_run)
46
+
37
47
  elif job.call:
38
- args = [*shlex.split(job.call), *args]
39
- it = iter_calls(args, iter_batches(job), dry_run=dry_run)
48
+ call(job, args, dry_run=dry_run)
49
+
40
50
  else:
41
51
  typer.echo(f"No command found in job: {job.name}.")
42
52
  raise Exit(1)
43
53
 
44
- if not dry_run:
45
- import mlflow
46
54
 
47
- mlflow.set_experiment(job.name)
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
48
59
 
49
- for task in it: # jobs will be executed here
50
- if job.run and dry_run:
51
- typer.echo(shlex.join(task.args))
52
- elif job.call and dry_run:
53
- funcname, *args = task.args
54
- arg = ", ".join(f"{arg!r}" for arg in args)
55
- typer.echo(f"{funcname}([{arg}])")
60
+ args = [*shlex.split(job.run), *args]
61
+ it = iter_tasks(args, iter_batches(job))
56
62
 
63
+ if not dry_run:
64
+ aio.run(it)
65
+ raise Exit
57
66
 
58
- @app.command(context_settings={"ignore_unknown_options": True})
59
- def submit(
60
- name: Annotated[str, Argument(help="Job name.", show_default=False)],
61
- *,
62
- args: Annotated[
63
- list[str] | None,
64
- Argument(help="Arguments to pass to the job.", show_default=False),
65
- ] = None,
66
- dry_run: Annotated[
67
- bool,
68
- Option("--dry-run", help="Perform a dry run."),
69
- ] = False,
70
- ) -> None:
71
- """Submit a job."""
72
- from hydraflow.executor.io import get_job
73
- from hydraflow.executor.job import iter_batches, submit
67
+ for task in it:
68
+ typer.echo(shlex.join(task.args))
74
69
 
75
- args = args or []
76
- job = get_job(name)
77
70
 
78
- if not job.run:
79
- typer.echo(f"No run found in job: {job.name}.")
80
- raise Exit(1)
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
74
+
75
+ args = [*shlex.split(job.call), *args]
76
+ it = iter_calls(args, iter_batches(job))
81
77
 
82
78
  if not dry_run:
83
- import mlflow
79
+ for call in it:
80
+ call.func()
81
+ raise Exit
84
82
 
85
- 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}])")
86
87
 
87
- args = [*shlex.split(job.run), *args]
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
92
+
93
+ args = [*shlex.split(job.submit), *args]
88
94
  result = submit(args, iter_batches(job), dry_run=dry_run)
89
95
 
90
96
  if dry_run and isinstance(result, tuple):
@@ -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())
@@ -15,6 +15,7 @@ class Job:
15
15
  name: str = ""
16
16
  run: str = ""
17
17
  call: str = ""
18
+ submit: str = ""
18
19
  with_: str = ""
19
20
  steps: list[Step] = field(default_factory=list)
20
21
 
@@ -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(
@@ -28,7 +28,7 @@ jobs:
28
28
  - batch: name=c,d
29
29
  args: count=4:6
30
30
  submit:
31
- run: python submit.py
31
+ submit: python submit.py
32
32
  steps:
33
33
  - batch: name=a,b
34
34
  args: count=1
@@ -54,7 +54,7 @@ def test_run_echo_dry_run():
54
54
 
55
55
 
56
56
  def test_submit_dry_run():
57
- args = ["submit", "submit", "--dry-run", "a", "--b", "--", "--dry-run"]
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
60
  assert result.stdout.count("submit.py a --b --dry-run --multirun") == 4
@@ -83,6 +83,11 @@ def test_run_parallel():
83
83
  run_ids = hydraflow.list_run_ids("parallel")
84
84
  assert len(run_ids) == 8
85
85
 
86
+ result = runner.invoke(app, ["run", "parallel"]) # skip if already run
87
+ assert result.exit_code == 0
88
+ run_ids = hydraflow.list_run_ids("parallel")
89
+ assert len(run_ids) == 8
90
+
86
91
 
87
92
  @pytest.mark.xdist_group(name="group4")
88
93
  def test_run_echo():
@@ -99,7 +104,7 @@ def test_run_echo():
99
104
 
100
105
  @pytest.mark.xdist_group(name="group4")
101
106
  def test_submit():
102
- result = runner.invoke(app, ["submit", "submit"])
107
+ result = runner.invoke(app, ["run", "submit"])
103
108
  assert result.exit_code == 0
104
109
  out = result.stdout
105
110
  lines = out.splitlines()
@@ -107,16 +112,14 @@ def test_submit():
107
112
  run_ids = hydraflow.list_run_ids("submit")
108
113
  assert len(run_ids) == 4
109
114
 
115
+ result = runner.invoke(app, ["run", "submit"]) # skip if already run
116
+ assert result.exit_code == 0
117
+ run_ids = hydraflow.list_run_ids("submit")
118
+ assert len(run_ids) == 4
119
+
110
120
 
111
121
  @pytest.mark.xdist_group(name="group5")
112
122
  def test_run_error():
113
123
  result = runner.invoke(app, ["run", "error"])
114
124
  assert result.exit_code == 1
115
125
  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
@@ -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