hydraflow 0.10.0__tar.gz → 0.10.2__tar.gz

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