hydraflow 0.11.1__tar.gz → 0.12.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 (93) hide show
  1. {hydraflow-0.11.1 → hydraflow-0.12.1}/PKG-INFO +1 -1
  2. {hydraflow-0.11.1 → hydraflow-0.12.1}/apps/quickstart.py +10 -10
  3. {hydraflow-0.11.1 → hydraflow-0.12.1}/docs/usage/quickstart.md +41 -14
  4. {hydraflow-0.11.1 → hydraflow-0.12.1}/mkdocs.yaml +1 -1
  5. {hydraflow-0.11.1 → hydraflow-0.12.1}/pyproject.toml +8 -2
  6. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/core/main.py +2 -1
  7. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/executor/conf.py +2 -2
  8. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/executor/io.py +21 -2
  9. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/executor/job.py +2 -2
  10. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/executor/parser.py +104 -44
  11. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/cli/hydraflow.yaml +2 -2
  12. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/executor/test_conf.py +4 -3
  13. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/executor/test_io.py +13 -0
  14. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/executor/test_parser.py +24 -0
  15. {hydraflow-0.11.1 → hydraflow-0.12.1}/.cursorrules +0 -0
  16. {hydraflow-0.11.1 → hydraflow-0.12.1}/.devcontainer/devcontainer.json +0 -0
  17. {hydraflow-0.11.1 → hydraflow-0.12.1}/.devcontainer/postCreate.sh +0 -0
  18. {hydraflow-0.11.1 → hydraflow-0.12.1}/.devcontainer/starship.toml +0 -0
  19. {hydraflow-0.11.1 → hydraflow-0.12.1}/.gitattributes +0 -0
  20. {hydraflow-0.11.1 → hydraflow-0.12.1}/.github/workflows/ci.yaml +0 -0
  21. {hydraflow-0.11.1 → hydraflow-0.12.1}/.github/workflows/docs.yaml +0 -0
  22. {hydraflow-0.11.1 → hydraflow-0.12.1}/.gitignore +0 -0
  23. {hydraflow-0.11.1 → hydraflow-0.12.1}/LICENSE +0 -0
  24. {hydraflow-0.11.1 → hydraflow-0.12.1}/README.md +0 -0
  25. {hydraflow-0.11.1 → hydraflow-0.12.1}/docs/index.md +0 -0
  26. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/__init__.py +0 -0
  27. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/cli.py +0 -0
  28. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/core/__init__.py +0 -0
  29. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/core/config.py +0 -0
  30. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/core/context.py +0 -0
  31. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/core/io.py +0 -0
  32. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/core/mlflow.py +0 -0
  33. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/core/param.py +0 -0
  34. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/entities/__init__.py +0 -0
  35. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/entities/run_collection.py +0 -0
  36. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/entities/run_data.py +0 -0
  37. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/entities/run_info.py +0 -0
  38. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/executor/__init__.py +0 -0
  39. {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/py.typed +0 -0
  40. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/__init__.py +0 -0
  41. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/cli/__init__.py +0 -0
  42. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/cli/app.py +0 -0
  43. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/cli/conftest.py +0 -0
  44. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/cli/test_run.py +0 -0
  45. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/cli/test_setup.py +0 -0
  46. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/cli/test_show.py +0 -0
  47. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/cli/test_version.py +0 -0
  48. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/conftest.py +0 -0
  49. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/__init__.py +0 -0
  50. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/config/__init__.py +0 -0
  51. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/config/test_config.py +0 -0
  52. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/config/test_params.py +0 -0
  53. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/context/__init__.py +0 -0
  54. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/context/chdir.py +0 -0
  55. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/context/log_run.py +0 -0
  56. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/context/start_run.py +0 -0
  57. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/context/test_chdir.py +0 -0
  58. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/context/test_log_run.py +0 -0
  59. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/context/test_start_run.py +0 -0
  60. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/io/__init__.py +0 -0
  61. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/io/hydra_dir.py +0 -0
  62. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/io/test_hydra_dir.py +0 -0
  63. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/io/test_iter_dirs.py +0 -0
  64. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/io/test_run.py +0 -0
  65. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/__init__.py +0 -0
  66. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/default.py +0 -0
  67. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/force_new_run.py +0 -0
  68. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/match_overrides.py +0 -0
  69. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/rerun_finished.py +0 -0
  70. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/skip_finished.py +0 -0
  71. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/test_default.py +0 -0
  72. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/test_force_new_run.py +0 -0
  73. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/test_match_overrides.py +0 -0
  74. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/test_rerun_finished.py +0 -0
  75. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/test_skip_finished.py +0 -0
  76. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/param/__init__.py +0 -0
  77. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/param/params.py +0 -0
  78. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/param/test_param.py +0 -0
  79. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/param/test_params.py +0 -0
  80. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/test_mlflow.py +0 -0
  81. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/entities/__init__.py +0 -0
  82. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/entities/filter.py +0 -0
  83. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/entities/test_collection.py +0 -0
  84. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/entities/test_data.py +0 -0
  85. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/entities/test_filter.py +0 -0
  86. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/entities/test_info.py +0 -0
  87. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/entities/test_values.py +0 -0
  88. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/entities/values.py +0 -0
  89. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/executor/__init__.py +0 -0
  90. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/executor/conftest.py +0 -0
  91. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/executor/echo.py +0 -0
  92. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/executor/test_args.py +0 -0
  93. {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/executor/test_job.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hydraflow
3
- Version: 0.11.1
3
+ Version: 0.12.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
@@ -1,13 +1,16 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING
3
6
 
4
- import hydra
5
- import mlflow
6
7
  from hydra.core.config_store import ConfigStore
7
- from hydra.core.hydra_config import HydraConfig
8
8
 
9
9
  import hydraflow
10
10
 
11
+ if TYPE_CHECKING:
12
+ from mlflow.entities import Run
13
+
11
14
  log = logging.getLogger(__name__)
12
15
 
13
16
 
@@ -21,13 +24,10 @@ cs = ConfigStore.instance()
21
24
  cs.store(name="config", node=Config)
22
25
 
23
26
 
24
- @hydra.main(config_name="config", version_base=None)
25
- def app(cfg: Config) -> None:
26
- hc = HydraConfig.get()
27
- mlflow.set_experiment(hc.job.name)
28
-
29
- with hydraflow.start_run(cfg):
30
- log.info(f"{cfg.width=}, {cfg.height=}")
27
+ @hydraflow.main(Config)
28
+ def app(run: Run, cfg: Config) -> None:
29
+ log.info(run.info.run_id)
30
+ log.info(cfg)
31
31
 
32
32
 
33
33
  if __name__ == "__main__":
@@ -2,24 +2,22 @@
2
2
 
3
3
  ## Hydra application
4
4
 
5
- The following example demonstrates how to use Hydraflow with a Hydra application.
6
- There are two main steps to using Hydraflow:
5
+ The following example demonstrates how to use a Hydraflow application.
7
6
 
8
- 1. Set the MLflow experiment using the Hydra job name.
9
- 2. Start a new MLflow run that logs the Hydra configuration.
10
-
11
- ```python title="apps/quickstart.py" linenums="1" hl_lines="24 26"
7
+ ```python title="apps/quickstart.py" linenums="1"
12
8
  --8<-- "apps/quickstart.py"
13
9
  ```
14
10
 
15
- ### Start a new MLflow run
11
+ ### Hydraflow's `main` decorator
16
12
 
17
- [`hydraflow.start_run`][] starts a new MLflow run that logs the Hydra configuration.
18
- It returns the started run so that it can be used to log metrics, parameters, and artifacts
19
- within the context of the run.
13
+ [`hydraflow.main`][] starts a new MLflow run that logs the Hydra configuration.
14
+ The decorated function must have two arguments: `Run` and `Config`.
15
+ The `Run` argument is the current MLflow run.
16
+ The `Config` argument is the Hydra configuration.
20
17
 
21
18
  ```python
22
- with hydraflow.start_run(cfg) as run:
19
+ @hydraflow.main(Config)
20
+ def app(run: Run, cfg: Config) -> None:
23
21
  pass
24
22
  ```
25
23
 
@@ -31,7 +29,7 @@ rm -rf mlruns outputs multirun
31
29
 
32
30
  ### Single-run
33
31
 
34
- Run the Hydra application as a normal Python script.
32
+ Run the Hydraflow application as a normal Python script.
35
33
 
36
34
  ```console exec="1" source="console"
37
35
  $ python apps/quickstart.py
@@ -43,8 +41,12 @@ Check the MLflow CLI to view the experiment.
43
41
  $ mlflow experiments search
44
42
  ```
45
43
 
44
+ The experiment name is the name of the Hydra job.
45
+
46
46
  ### Multi-run
47
47
 
48
+ Run the Hydraflow application with multiple configurations.
49
+
48
50
  ```console exec="1" source="console"
49
51
  $ python apps/quickstart.py -m width=400,600 height=100,200,300
50
52
  ```
@@ -53,6 +55,8 @@ $ python apps/quickstart.py -m width=400,600 height=100,200,300
53
55
 
54
56
  ### Run collection
55
57
 
58
+ The `RunCollection` object is a collection of runs.
59
+
56
60
  ```pycon exec="1" source="console" session="quickstart"
57
61
  >>> import hydraflow
58
62
  >>> rc = hydraflow.list_runs("quickstart")
@@ -61,35 +65,47 @@ $ python apps/quickstart.py -m width=400,600 height=100,200,300
61
65
 
62
66
  ### Retrieve a run
63
67
 
68
+ The `RunCollection` object has a `first` and `last` method that
69
+ returns the first and last run in the collection.
70
+
64
71
  ```pycon exec="1" source="console" session="quickstart"
65
72
  >>> run = rc.first()
66
73
  >>> print(type(run))
67
74
  ```
68
75
 
69
76
  ```pycon exec="1" source="console" session="quickstart"
77
+ >>> run = rc.last()
70
78
  >>> cfg = hydraflow.load_config(run)
71
- >>> print(type(cfg))
72
79
  >>> print(cfg)
73
80
  ```
74
81
 
82
+ The `load_config` function loads the Hydra configuration from the run.
83
+
75
84
  ```pycon exec="1" source="console" session="quickstart"
76
- >>> run = rc.last()
77
85
  >>> cfg = hydraflow.load_config(run)
86
+ >>> print(type(cfg))
78
87
  >>> print(cfg)
79
88
  ```
80
89
 
81
90
  ### Filter runs
82
91
 
92
+ The `filter` method filters the runs by the given key-value pairs.
93
+
83
94
  ```pycon exec="1" source="console" session="quickstart"
84
95
  >>> filtered = rc.filter(width=400)
85
96
  >>> print(filtered)
86
97
  ```
87
98
 
99
+ If the value is a list, the run will be included if the value is in the list.
100
+
88
101
  ```pycon exec="1" source="console" session="quickstart"
89
102
  >>> filtered = rc.filter(height=[100, 300])
90
103
  >>> print(filtered)
91
104
  ```
92
105
 
106
+ If the value is a tuple, the run will be included if the value is between the tuple.
107
+ The start and end of the tuple are inclusive.
108
+
93
109
  ```pycon exec="1" source="console" session="quickstart"
94
110
  >>> filtered = rc.filter(height=(100, 300))
95
111
  >>> print(filtered)
@@ -97,12 +113,16 @@ $ python apps/quickstart.py -m width=400,600 height=100,200,300
97
113
 
98
114
  ### Group runs
99
115
 
116
+ The `groupby` method groups the runs by the given key.
117
+
100
118
  ```pycon exec="1" source="console" session="quickstart"
101
119
  >>> grouped = rc.groupby("width")
102
120
  >>> for key, group in grouped.items():
103
121
  ... print(key, group)
104
122
  ```
105
123
 
124
+ The `groupby` method can also take a list of keys.
125
+
106
126
  ```pycon exec="1" source="console" session="quickstart"
107
127
  >>> grouped = rc.groupby(["height"])
108
128
  >>> for key, group in grouped.items():
@@ -111,6 +131,13 @@ $ python apps/quickstart.py -m width=400,600 height=100,200,300
111
131
 
112
132
  ### Config dataframe
113
133
 
134
+ The `data.config` attribute returns a pandas DataFrame
135
+ of the Hydra configuration.
136
+
114
137
  ```pycon exec="1" source="console" session="quickstart"
115
138
  >>> print(rc.data.config)
116
139
  ```
140
+
141
+ ```bash exec="on"
142
+ rm -rf mlruns outputs multirun
143
+ ```
@@ -53,4 +53,4 @@ nav:
53
53
  - Home: index.md
54
54
  - Usage:
55
55
  - usage/quickstart.md
56
- - Reference: $api/hydraflow.**
56
+ - Reference: $api/hydraflow.***
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hydraflow"
7
- version = "0.11.1"
7
+ version = "0.12.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" }
@@ -96,8 +96,14 @@ ignore = [
96
96
  ]
97
97
 
98
98
  [tool.ruff.lint.per-file-ignores]
99
- "apps/*.py" = ["D", "G", "INP"]
99
+ "apps/*.py" = ["D", "G", "INP", "T"]
100
100
  "src/hydraflow/cli.py" = ["ANN", "D"]
101
101
  "src/hydraflow/core/main.py" = ["ANN201", "D401"]
102
102
  "src/hydraflow/executor/conf.py" = ["ANN", "D"]
103
103
  "tests/*" = ["A001", "ANN", "ARG", "D", "FBT", "PD", "PLR", "PT", "S", "SLF"]
104
+
105
+ [tool.pyright]
106
+ include = ["src", "tests"]
107
+ strictDictionaryInference = true
108
+ strictListInference = true
109
+ strictSetInference = true
@@ -16,6 +16,7 @@ used to wrap experiment entry points. This decorator handles:
16
16
 
17
17
  Example:
18
18
  ```python
19
+ import hydraflow
19
20
  from dataclasses import dataclass
20
21
  from mlflow.entities import Run
21
22
 
@@ -24,7 +25,7 @@ Example:
24
25
  learning_rate: float
25
26
  batch_size: int
26
27
 
27
- @main(Config)
28
+ @hydraflow.main(Config)
28
29
  def train(run: Run, config: Config):
29
30
  # Your training code here
30
31
  pass
@@ -7,7 +7,7 @@ from dataclasses import dataclass, field
7
7
  class Step:
8
8
  batch: str = ""
9
9
  args: str = ""
10
- configs: str = ""
10
+ with_: str = ""
11
11
 
12
12
 
13
13
  @dataclass
@@ -15,7 +15,7 @@ class Job:
15
15
  name: str = ""
16
16
  run: str = ""
17
17
  call: str = ""
18
- configs: str = ""
18
+ with_: str = ""
19
19
  steps: list[Step] = field(default_factory=list)
20
20
 
21
21
 
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from pathlib import Path
6
6
  from typing import TYPE_CHECKING
7
7
 
8
- from omegaconf import OmegaConf
8
+ from omegaconf import DictConfig, ListConfig, OmegaConf
9
9
 
10
10
  from .conf import HydraflowConf
11
11
 
@@ -35,7 +35,26 @@ def load_config() -> HydraflowConf:
35
35
 
36
36
  cfg = OmegaConf.load(path)
37
37
 
38
- return OmegaConf.merge(schema, cfg) # type: ignore
38
+ if not isinstance(cfg, DictConfig):
39
+ return schema
40
+
41
+ rename_with(cfg)
42
+
43
+ return OmegaConf.merge(schema, cfg) # type: ignore[return-value]
44
+
45
+
46
+ def rename_with(cfg: DictConfig) -> None:
47
+ """Rename the `with` field to `with_`."""
48
+ if "with" in cfg:
49
+ cfg["with_"] = cfg.pop("with")
50
+
51
+ for key in list(cfg.keys()):
52
+ if isinstance(cfg[key], DictConfig):
53
+ rename_with(cfg[key])
54
+ elif isinstance(cfg[key], ListConfig):
55
+ for item in cfg[key]:
56
+ if isinstance(item, DictConfig):
57
+ rename_with(item)
39
58
 
40
59
 
41
60
  def get_job(name: str) -> Job:
@@ -69,10 +69,10 @@ def iter_batches(job: Job) -> Iterator[list[str]]:
69
69
 
70
70
  """
71
71
  job_name = f"hydra.job.name={job.name}"
72
- job_configs = shlex.split(job.configs)
72
+ job_configs = shlex.split(job.with_)
73
73
 
74
74
  for step in job.steps:
75
- configs = shlex.split(step.configs) or job_configs
75
+ configs = shlex.split(step.with_) or job_configs
76
76
 
77
77
  for args in iter_args(step.batch, step.args):
78
78
  sweep_dir = f"hydra.sweep.dir=multirun/{ulid.ULID()}"
@@ -83,7 +83,7 @@ def count_decimal_places(x: str) -> int:
83
83
 
84
84
 
85
85
  def is_number(x: str) -> bool:
86
- """Check if a string is a number.
86
+ """Check if a string represents a valid number.
87
87
 
88
88
  Args:
89
89
  x (str): The string to check.
@@ -140,18 +140,54 @@ def _get_range(arg: str) -> tuple[float, float, float]:
140
140
 
141
141
 
142
142
  def _arange(start: float, step: float, stop: float) -> list[float]:
143
+ """Generate a range of floating point numbers.
144
+
145
+ This function generates a range of floating point numbers
146
+ with protection against rounding errors.
147
+
148
+ Args:
149
+ start (float): The starting value.
150
+ step (float): The step size.
151
+ stop (float): The end value (inclusive).
152
+
153
+ Returns:
154
+ list[float]: A list of floating point numbers from start to stop
155
+ (inclusive) with the given step.
156
+
157
+ """
158
+ if step == 0:
159
+ raise ValueError("Step cannot be zero")
160
+
161
+ epsilon = 1e-10
162
+
163
+ decimal_places = max(
164
+ count_decimal_places(str(start)),
165
+ count_decimal_places(str(step)),
166
+ count_decimal_places(str(stop)),
167
+ )
168
+
143
169
  result = []
144
170
  current = start
145
171
 
146
- while current <= stop if step > 0 else current >= stop:
147
- result.append(current)
148
- current += step
172
+ if step > 0:
173
+ while current <= stop + epsilon:
174
+ rounded = round(current, decimal_places)
175
+ result.append(rounded)
176
+ current += step
177
+ else:
178
+ while current >= stop - epsilon:
179
+ rounded = round(current, decimal_places)
180
+ result.append(rounded)
181
+ current += step
149
182
 
150
183
  return result
151
184
 
152
185
 
153
186
  def split_suffix(arg: str) -> tuple[str, str]:
154
- """Split a string into prefix and suffix.
187
+ """Split a string into the prefix and suffix.
188
+
189
+ The suffix is the part of the string that starts with a colon (:).
190
+ The prefix is the part of the string that precedes the suffix.
155
191
 
156
192
  Args:
157
193
  arg (str): The string to split.
@@ -194,6 +230,16 @@ def add_exponent(value: str, exponent: str) -> str:
194
230
  Returns:
195
231
  str: The value with the exponent added.
196
232
 
233
+ Examples:
234
+ >>> add_exponent("1", "e3")
235
+ '1e3'
236
+ >>> add_exponent("1", "")
237
+ '1'
238
+ >>> add_exponent("0", "e-3")
239
+ '0'
240
+ >>> add_exponent("0.0", "e-3")
241
+ '0.0'
242
+
197
243
  """
198
244
  if value in ["0", "0.", "0.0"] or not exponent:
199
245
  return value
@@ -201,43 +247,6 @@ def add_exponent(value: str, exponent: str) -> str:
201
247
  return f"{value}{exponent}"
202
248
 
203
249
 
204
- def collect_values(arg: str) -> list[str]:
205
- """Collect a list of values from a range argument.
206
-
207
- Collect all individual values within a numeric range
208
- represented by a string (e.g., `1:4`) and return them
209
- as a list of strings.
210
- Support both integer and floating-point ranges.
211
-
212
- Args:
213
- arg (str): The argument to collect.
214
-
215
- Returns:
216
- list[str]: A list of the collected values.
217
-
218
- """
219
- if "(" in arg:
220
- return collect_parentheses(arg)
221
-
222
- if ":" not in arg:
223
- return [arg]
224
-
225
- arg, exponent = split_suffix(arg)
226
-
227
- if ":" not in arg:
228
- return [f"{arg}{exponent}"]
229
-
230
- rng = _get_range(arg)
231
-
232
- if all(isinstance(x, int) for x in rng):
233
- values = [str(x) for x in _arange(*rng)]
234
- else:
235
- n = max(*(count_decimal_places(x) for x in arg.split(":")))
236
- values = [str(round(x, n)) for x in _arange(*rng)]
237
-
238
- return [add_exponent(x, exponent) for x in values]
239
-
240
-
241
250
  def split_parentheses(arg: str) -> Iterator[str]:
242
251
  """Split a string with parentheses into a list of strings.
243
252
 
@@ -290,8 +299,59 @@ def collect_parentheses(arg: str) -> list[str]:
290
299
  return ["".join(x[::-1]) for x in product(*it[::-1])]
291
300
 
292
301
 
302
+ def collect_values(arg: str) -> list[str]:
303
+ """Collect a list of values from a range argument.
304
+
305
+ Collect all individual values within a numeric range
306
+ represented by a string (e.g., `1:4`) and return them
307
+ as a list of strings.
308
+ Support both integer and floating-point ranges.
309
+
310
+ Args:
311
+ arg (str): The argument to collect.
312
+
313
+ Returns:
314
+ list[str]: A list of the collected values.
315
+
316
+ Examples:
317
+ >>> collect_values("1:4")
318
+ ['1', '2', '3', '4']
319
+ >>> collect_values("1.2:0.1:1.4:k")
320
+ ['1.2e3', '1.3e3', '1.4e3']
321
+ >>> collect_values("0.1")
322
+ ['0.1']
323
+ >>> collect_values("4:M")
324
+ ['4e6']
325
+ >>> collect_values("(1:3,5:7)M")
326
+ ['1e6', '2e6', '3e6', '5e6', '6e6', '7e6']
327
+ >>> collect_values("(1,5)e-(1:3)")
328
+ ['1e-1', '5e-1', '1e-2', '5e-2', '1e-3', '5e-3']
329
+
330
+ """
331
+ if "(" in arg:
332
+ return collect_parentheses(arg)
333
+
334
+ if ":" not in arg:
335
+ return [arg]
336
+
337
+ arg, exponent = split_suffix(arg)
338
+
339
+ if ":" not in arg:
340
+ return [f"{arg}{exponent}"]
341
+
342
+ rng = _get_range(arg)
343
+
344
+ if all(isinstance(x, int) for x in rng):
345
+ values = [str(x) for x in _arange(*rng)]
346
+ else:
347
+ n = max(*(count_decimal_places(x) for x in arg.split(":")))
348
+ values = [str(round(x, n)) for x in _arange(*rng)]
349
+
350
+ return [add_exponent(x, exponent) for x in values]
351
+
352
+
293
353
  def split(arg: str) -> list[str]:
294
- r"""Split a string by top-level commas.
354
+ """Split a string by top-level commas.
295
355
 
296
356
  Splits a string by commas while respecting nested structures.
297
357
  Commas inside brackets and quotes are ignored, only splitting
@@ -389,7 +449,7 @@ def split_arg(arg: str) -> tuple[str, str, str]:
389
449
  key, value = arg.split("=")
390
450
 
391
451
  if "/" in key:
392
- key, suffix = key.split("/", 1)
452
+ key, suffix = key.split("/")
393
453
  return key, suffix, value
394
454
 
395
455
  return key, "", value
@@ -13,10 +13,10 @@ jobs:
13
13
  args: count=100
14
14
  parallel:
15
15
  run: python app.py
16
- configs: hydra/launcher=joblib hydra.launcher.n_jobs=2
16
+ with: hydra/launcher=joblib hydra.launcher.n_jobs=2
17
17
  steps:
18
18
  - batch: name=a
19
19
  args: count=1:4
20
20
  - batch: name=b
21
21
  args: count=11:14
22
- configs: hydra/launcher=joblib hydra.launcher.n_jobs=4
22
+ with: hydra/launcher=joblib hydra.launcher.n_jobs=4
@@ -25,10 +25,11 @@ def test_none():
25
25
 
26
26
 
27
27
  def test_job(config):
28
- cfg = config("jobs:\n a:\n run: a.test\n")
28
+ cfg = config("jobs:\n a:\n run: a.test\n with: --opt1 --opt2\n")
29
29
  assert cfg.jobs["a"].run == "a.test"
30
+ assert cfg.jobs["a"].with_ == "--opt1 --opt2"
30
31
 
31
32
 
32
33
  def test_step(config):
33
- cfg = config("jobs:\n a:\n steps:\n - configs: --opt1 --opt2\n")
34
- assert cfg.jobs["a"].steps[0].configs == "--opt1 --opt2"
34
+ cfg = config("jobs:\n a:\n steps:\n - with: --opt1 --opt2\n")
35
+ assert cfg.jobs["a"].steps[0].with_ == "--opt1 --opt2"
@@ -1,6 +1,7 @@
1
1
  from pathlib import Path
2
2
 
3
3
  import pytest
4
+ from omegaconf import DictConfig
4
5
 
5
6
 
6
7
  @pytest.mark.parametrize("file", ["hydraflow.yaml", "hydraflow.yml"])
@@ -16,3 +17,15 @@ def test_find_config_none(chdir):
16
17
  from hydraflow.executor.io import find_config_file
17
18
 
18
19
  assert find_config_file() is None
20
+
21
+
22
+ def test_load_config_list(chdir):
23
+ from hydraflow.executor.io import load_config
24
+
25
+ Path("hydraflow.yaml").write_text("- a\n- b\n")
26
+
27
+ cfg = load_config()
28
+ assert isinstance(cfg, DictConfig)
29
+ assert cfg.jobs == {}
30
+
31
+ Path("hydraflow.yaml").unlink()
@@ -24,6 +24,7 @@ def test_count_decimal_places(s, x):
24
24
  ("1:2", (1, 1, 2)),
25
25
  (":2", (0, 1, 2)),
26
26
  ("0.1:0.1:0.4", (0.1, 0.1, 0.4)),
27
+ ("1.2:0.1:1.4", (1.2, 0.1, 1.4)),
27
28
  ],
28
29
  )
29
30
  def test_get_range(s, x):
@@ -49,6 +50,28 @@ def test_get_range_errors(arg, expected_exception, expected_message):
49
50
  assert str(excinfo.value) == expected_message
50
51
 
51
52
 
53
+ @pytest.mark.parametrize(
54
+ ("start", "step", "stop", "expected"),
55
+ [
56
+ (1.0, 2.0, 5.0, [1.0, 3.0, 5.0]),
57
+ (1.2, 0.1, 1.4, [1.2, 1.3, 1.4]),
58
+ (1.4, -0.1, 1.2, [1.4, 1.3, 1.2]),
59
+ (1.02e-3, 0.01e-3, 1.04e-3, [0.00102, 0.00103, 0.00104]),
60
+ ],
61
+ )
62
+ def test_arange(start, step, stop, expected):
63
+ from hydraflow.executor.parser import _arange
64
+
65
+ assert _arange(start, step, stop) == expected
66
+
67
+
68
+ def test_arange_error():
69
+ from hydraflow.executor.parser import _arange
70
+
71
+ with pytest.raises(ValueError):
72
+ _arange(1.0, 0.0, 1.0)
73
+
74
+
52
75
  @pytest.mark.parametrize(
53
76
  ("s", "x"),
54
77
  [
@@ -100,6 +123,7 @@ def test_split_suffix(s, x):
100
123
  ("4.5:-1.5:-4.5", ["4.5", "3.0", "1.5", "0.0", "-1.5", "-3.0", "-4.5"]),
101
124
  ("1:2:u", ["1e-6", "2e-6"]),
102
125
  ("1:.25:2:n", ["1e-9", "1.25e-9", "1.5e-9", "1.75e-9", "2.0e-9"]),
126
+ ("1.2:0.1:1.4:k", ["1.2e3", "1.3e3", "1.4e3"]),
103
127
  ("1:2:e2", ["1e2", "2e2"]),
104
128
  (":2:e2", ["0", "1e2", "2e2"]),
105
129
  ("-2:2:k", ["-2e3", "-1e3", "0", "1e3", "2e3"]),
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