hydraflow 0.12.0__tar.gz → 0.12.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 (93) hide show
  1. {hydraflow-0.12.0 → hydraflow-0.12.2}/PKG-INFO +1 -1
  2. {hydraflow-0.12.0 → hydraflow-0.12.2}/apps/quickstart.py +10 -10
  3. {hydraflow-0.12.0 → hydraflow-0.12.2}/docs/usage/quickstart.md +41 -14
  4. {hydraflow-0.12.0 → hydraflow-0.12.2}/mkdocs.yaml +1 -1
  5. {hydraflow-0.12.0 → hydraflow-0.12.2}/pyproject.toml +5 -2
  6. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/core/main.py +2 -1
  7. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/executor/parser.py +153 -53
  8. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/executor/test_parser.py +56 -14
  9. {hydraflow-0.12.0 → hydraflow-0.12.2}/.cursorrules +0 -0
  10. {hydraflow-0.12.0 → hydraflow-0.12.2}/.devcontainer/devcontainer.json +0 -0
  11. {hydraflow-0.12.0 → hydraflow-0.12.2}/.devcontainer/postCreate.sh +0 -0
  12. {hydraflow-0.12.0 → hydraflow-0.12.2}/.devcontainer/starship.toml +0 -0
  13. {hydraflow-0.12.0 → hydraflow-0.12.2}/.gitattributes +0 -0
  14. {hydraflow-0.12.0 → hydraflow-0.12.2}/.github/workflows/ci.yaml +0 -0
  15. {hydraflow-0.12.0 → hydraflow-0.12.2}/.github/workflows/docs.yaml +0 -0
  16. {hydraflow-0.12.0 → hydraflow-0.12.2}/.gitignore +0 -0
  17. {hydraflow-0.12.0 → hydraflow-0.12.2}/LICENSE +0 -0
  18. {hydraflow-0.12.0 → hydraflow-0.12.2}/README.md +0 -0
  19. {hydraflow-0.12.0 → hydraflow-0.12.2}/docs/index.md +0 -0
  20. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/__init__.py +0 -0
  21. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/cli.py +0 -0
  22. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/core/__init__.py +0 -0
  23. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/core/config.py +0 -0
  24. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/core/context.py +0 -0
  25. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/core/io.py +0 -0
  26. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/core/mlflow.py +0 -0
  27. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/core/param.py +0 -0
  28. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/entities/__init__.py +0 -0
  29. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/entities/run_collection.py +0 -0
  30. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/entities/run_data.py +0 -0
  31. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/entities/run_info.py +0 -0
  32. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/executor/__init__.py +0 -0
  33. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/executor/conf.py +0 -0
  34. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/executor/io.py +0 -0
  35. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/executor/job.py +0 -0
  36. {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/py.typed +0 -0
  37. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/__init__.py +0 -0
  38. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/cli/__init__.py +0 -0
  39. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/cli/app.py +0 -0
  40. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/cli/conftest.py +0 -0
  41. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/cli/hydraflow.yaml +0 -0
  42. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/cli/test_run.py +0 -0
  43. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/cli/test_setup.py +0 -0
  44. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/cli/test_show.py +0 -0
  45. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/cli/test_version.py +0 -0
  46. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/conftest.py +0 -0
  47. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/__init__.py +0 -0
  48. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/config/__init__.py +0 -0
  49. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/config/test_config.py +0 -0
  50. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/config/test_params.py +0 -0
  51. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/context/__init__.py +0 -0
  52. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/context/chdir.py +0 -0
  53. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/context/log_run.py +0 -0
  54. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/context/start_run.py +0 -0
  55. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/context/test_chdir.py +0 -0
  56. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/context/test_log_run.py +0 -0
  57. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/context/test_start_run.py +0 -0
  58. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/io/__init__.py +0 -0
  59. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/io/hydra_dir.py +0 -0
  60. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/io/test_hydra_dir.py +0 -0
  61. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/io/test_iter_dirs.py +0 -0
  62. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/io/test_run.py +0 -0
  63. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/__init__.py +0 -0
  64. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/default.py +0 -0
  65. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/force_new_run.py +0 -0
  66. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/match_overrides.py +0 -0
  67. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/rerun_finished.py +0 -0
  68. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/skip_finished.py +0 -0
  69. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/test_default.py +0 -0
  70. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/test_force_new_run.py +0 -0
  71. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/test_match_overrides.py +0 -0
  72. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/test_rerun_finished.py +0 -0
  73. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/test_skip_finished.py +0 -0
  74. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/param/__init__.py +0 -0
  75. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/param/params.py +0 -0
  76. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/param/test_param.py +0 -0
  77. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/param/test_params.py +0 -0
  78. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/test_mlflow.py +0 -0
  79. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/entities/__init__.py +0 -0
  80. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/entities/filter.py +0 -0
  81. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/entities/test_collection.py +0 -0
  82. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/entities/test_data.py +0 -0
  83. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/entities/test_filter.py +0 -0
  84. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/entities/test_info.py +0 -0
  85. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/entities/test_values.py +0 -0
  86. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/entities/values.py +0 -0
  87. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/executor/__init__.py +0 -0
  88. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/executor/conftest.py +0 -0
  89. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/executor/echo.py +0 -0
  90. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/executor/test_args.py +0 -0
  91. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/executor/test_conf.py +0 -0
  92. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/executor/test_io.py +0 -0
  93. {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/executor/test_job.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hydraflow
3
- Version: 0.12.0
3
+ Version: 0.12.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
@@ -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.12.0"
7
+ version = "0.12.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" }
@@ -96,7 +96,7 @@ 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"]
@@ -104,3 +104,6 @@ ignore = [
104
104
 
105
105
  [tool.pyright]
106
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
@@ -48,7 +48,7 @@ def to_number(x: str) -> int | float:
48
48
  return int(x)
49
49
 
50
50
 
51
- def count_decimal_places(x: str) -> int:
51
+ def count_decimal_digits(x: str) -> int:
52
52
  """Count decimal places in a string.
53
53
 
54
54
  Examine a string representing a number and returns the count
@@ -62,13 +62,13 @@ def count_decimal_places(x: str) -> int:
62
62
  int: The number of decimal places.
63
63
 
64
64
  Examples:
65
- >>> count_decimal_places("1")
65
+ >>> count_decimal_digits("1")
66
66
  0
67
- >>> count_decimal_places("-1.2")
67
+ >>> count_decimal_digits("-1.2")
68
68
  1
69
- >>> count_decimal_places("1.234")
69
+ >>> count_decimal_digits("1.234")
70
70
  3
71
- >>> count_decimal_places("-1.234e-10")
71
+ >>> count_decimal_digits("-1.234e-10")
72
72
  3
73
73
 
74
74
  """
@@ -82,8 +82,52 @@ def count_decimal_places(x: str) -> int:
82
82
  return len(decimal_part)
83
83
 
84
84
 
85
+ def count_integer_digits(num_str: str) -> int:
86
+ """Count the number of digits in the integer part of a number.
87
+
88
+ Consider only the integer part of a number, even if it is in
89
+ scientific notation.
90
+
91
+ Args:
92
+ num_str (str): The string representing a number.
93
+
94
+ Returns:
95
+ int: The number of digits in the integer part of a number.
96
+ (excluding the sign)
97
+
98
+ Examples:
99
+ >>> count_integer_digits("123")
100
+ 3
101
+ >>> count_integer_digits("-123.45")
102
+ 3
103
+ >>> count_integer_digits("+0.00123")
104
+ 1
105
+ >>> count_integer_digits("-1.200")
106
+ 1
107
+ >>> count_integer_digits("+1.20e3")
108
+ 1
109
+ >>> count_integer_digits("-0.120e-3")
110
+ 1
111
+ >>> count_integer_digits(".123")
112
+ 0
113
+
114
+ """
115
+ if num_str.startswith(("+", "-")):
116
+ num_str = num_str[1:]
117
+
118
+ if "e" in num_str.lower():
119
+ num_str = num_str.lower().split("e")[0]
120
+
121
+ if "." in num_str:
122
+ int_part = num_str.split(".")[0]
123
+ if not int_part:
124
+ return 0
125
+ return len(int_part)
126
+ return len(num_str)
127
+
128
+
85
129
  def is_number(x: str) -> bool:
86
- """Check if a string is a number.
130
+ """Check if a string represents a valid number.
87
131
 
88
132
  Args:
89
133
  x (str): The string to check.
@@ -140,18 +184,46 @@ def _get_range(arg: str) -> tuple[float, float, float]:
140
184
 
141
185
 
142
186
  def _arange(start: float, step: float, stop: float) -> list[float]:
187
+ """Generate a range of floating point numbers.
188
+
189
+ This function generates a range of floating point numbers
190
+ with protection against rounding errors.
191
+
192
+ Args:
193
+ start (float): The starting value.
194
+ step (float): The step size.
195
+ stop (float): The end value (inclusive).
196
+
197
+ Returns:
198
+ list[float]: A list of floating point numbers from start to stop
199
+ (inclusive) with the given step.
200
+
201
+ """
202
+ if step == 0:
203
+ raise ValueError("Step cannot be zero")
204
+
205
+ epsilon = min(abs(start), abs(stop)) * 1e-5
206
+
143
207
  result = []
144
208
  current = start
145
209
 
146
- while current <= stop if step > 0 else current >= stop:
147
- result.append(current)
148
- current += step
210
+ if step > 0:
211
+ while current <= stop + epsilon:
212
+ result.append(current)
213
+ current += step
214
+ else:
215
+ while current >= stop - epsilon:
216
+ result.append(current)
217
+ current += step
149
218
 
150
219
  return result
151
220
 
152
221
 
153
222
  def split_suffix(arg: str) -> tuple[str, str]:
154
- """Split a string into prefix and suffix.
223
+ """Split a string into the prefix and suffix.
224
+
225
+ The suffix is the part of the string that starts with a colon (:).
226
+ The prefix is the part of the string that precedes the suffix.
155
227
 
156
228
  Args:
157
229
  arg (str): The string to split.
@@ -194,6 +266,16 @@ def add_exponent(value: str, exponent: str) -> str:
194
266
  Returns:
195
267
  str: The value with the exponent added.
196
268
 
269
+ Examples:
270
+ >>> add_exponent("1", "e3")
271
+ '1e3'
272
+ >>> add_exponent("1", "")
273
+ '1'
274
+ >>> add_exponent("0", "e-3")
275
+ '0'
276
+ >>> add_exponent("0.0", "e-3")
277
+ '0.0'
278
+
197
279
  """
198
280
  if value in ["0", "0.", "0.0"] or not exponent:
199
281
  return value
@@ -201,43 +283,6 @@ def add_exponent(value: str, exponent: str) -> str:
201
283
  return f"{value}{exponent}"
202
284
 
203
285
 
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
286
  def split_parentheses(arg: str) -> Iterator[str]:
242
287
  """Split a string with parentheses into a list of strings.
243
288
 
@@ -290,8 +335,60 @@ def collect_parentheses(arg: str) -> list[str]:
290
335
  return ["".join(x[::-1]) for x in product(*it[::-1])]
291
336
 
292
337
 
338
+ def collect_values(arg: str) -> list[str]:
339
+ """Collect a list of values from a range argument.
340
+
341
+ Collect all individual values within a numeric range
342
+ represented by a string (e.g., `1:4`) and return them
343
+ as a list of strings.
344
+ Support both integer and floating-point ranges.
345
+
346
+ Args:
347
+ arg (str): The argument to collect.
348
+
349
+ Returns:
350
+ list[str]: A list of the collected values.
351
+
352
+ Examples:
353
+ >>> collect_values("1:4")
354
+ ['1', '2', '3', '4']
355
+ >>> collect_values("1.2:0.1:1.4:k")
356
+ ['1.2e3', '1.3e3', '1.4e3']
357
+ >>> collect_values("0.1")
358
+ ['0.1']
359
+ >>> collect_values("4:M")
360
+ ['4e6']
361
+ >>> collect_values("(1:3,5:7)M")
362
+ ['1e6', '2e6', '3e6', '5e6', '6e6', '7e6']
363
+ >>> collect_values("(1,5)e-(1:3)")
364
+ ['1e-1', '5e-1', '1e-2', '5e-2', '1e-3', '5e-3']
365
+
366
+ """
367
+ if "(" in arg:
368
+ return collect_parentheses(arg)
369
+
370
+ if ":" not in arg:
371
+ return [arg]
372
+
373
+ arg, exponent = split_suffix(arg)
374
+
375
+ if ":" not in arg:
376
+ return [f"{arg}{exponent}"]
377
+
378
+ rng = _get_range(arg)
379
+
380
+ if all(isinstance(x, int) for x in rng):
381
+ values = [str(x) for x in _arange(*rng)]
382
+ else:
383
+ n = max(*(count_integer_digits(x) for x in arg.split(":")))
384
+ m = max(*(count_decimal_digits(x) for x in arg.split(":")))
385
+ values = [f"{x:.{n + m}g}" for x in _arange(*rng)]
386
+
387
+ return [add_exponent(x, exponent) for x in values]
388
+
389
+
293
390
  def split(arg: str) -> list[str]:
294
- r"""Split a string by top-level commas.
391
+ """Split a string by top-level commas.
295
392
 
296
393
  Splits a string by commas while respecting nested structures.
297
394
  Commas inside brackets and quotes are ignored, only splitting
@@ -355,8 +452,9 @@ def split(arg: str) -> list[str]:
355
452
  def expand_values(arg: str, suffix: str = "") -> Iterator[str]:
356
453
  """Expand a string argument into a list of values.
357
454
 
358
- Take a string containing comma-separated values or ranges and return a list
359
- of all individual values. Handle numeric ranges and special characters.
455
+ Take a string containing comma-separated values or ranges
456
+ and return a list of all individual values.
457
+ Handle numeric ranges and special characters.
360
458
 
361
459
  Args:
362
460
  arg (str): The argument to expand.
@@ -379,7 +477,8 @@ def split_arg(arg: str) -> tuple[str, str, str]:
379
477
  arg (str): The argument to split.
380
478
 
381
479
  Returns:
382
- tuple[str, str, str]: A tuple containing the key, suffix, and value.
480
+ tuple[str, str, str]: A tuple containing the key,
481
+ suffix, and value.
383
482
 
384
483
  """
385
484
  if "=" not in arg:
@@ -398,8 +497,9 @@ def split_arg(arg: str) -> tuple[str, str, str]:
398
497
  def collect_arg(arg: str) -> str:
399
498
  """Collect a string of expanded key-value pairs.
400
499
 
401
- Take a key-value pair argument and concatenates all expanded values with commas,
402
- returning a single string suitable for command-line usage.
500
+ Take a key-value pair argument and concatenates all expanded
501
+ values with commas, returning a single string suitable for
502
+ command-line usage.
403
503
 
404
504
  Args:
405
505
  arg (str): The argument to collect.
@@ -1,3 +1,4 @@
1
+ import numpy as np
1
2
  import pytest
2
3
 
3
4
 
@@ -12,10 +13,20 @@ def test_to_number(s, x):
12
13
  ("s", "x"),
13
14
  [("1", 0), ("1.2", 1), ("1.234", 3), ("123.", 0), ("", 0), ("1.234e-10", 3)],
14
15
  )
15
- def test_count_decimal_places(s, x):
16
- from hydraflow.executor.parser import count_decimal_places
16
+ def test_count_decimal_digits(s, x):
17
+ from hydraflow.executor.parser import count_decimal_digits
17
18
 
18
- assert count_decimal_places(s) == x
19
+ assert count_decimal_digits(s) == x
20
+
21
+
22
+ @pytest.mark.parametrize(
23
+ ("s", "x"),
24
+ [("1", 1), ("1.2", 1), ("1.234", 1), ("123.", 3), ("", 0), ("1.234e-10", 1)],
25
+ )
26
+ def test_count_integer_digits(s, x):
27
+ from hydraflow.executor.parser import count_integer_digits
28
+
29
+ assert count_integer_digits(s) == x
19
30
 
20
31
 
21
32
  @pytest.mark.parametrize(
@@ -24,6 +35,7 @@ def test_count_decimal_places(s, x):
24
35
  ("1:2", (1, 1, 2)),
25
36
  (":2", (0, 1, 2)),
26
37
  ("0.1:0.1:0.4", (0.1, 0.1, 0.4)),
38
+ ("1.2:0.1:1.4", (1.2, 0.1, 1.4)),
27
39
  ],
28
40
  )
29
41
  def test_get_range(s, x):
@@ -49,6 +61,29 @@ def test_get_range_errors(arg, expected_exception, expected_message):
49
61
  assert str(excinfo.value) == expected_message
50
62
 
51
63
 
64
+ @pytest.mark.parametrize(
65
+ ("start", "step", "stop", "expected"),
66
+ [
67
+ (1.0, 2.0, 5.0, [1.0, 3.0, 5.0]),
68
+ (1.2, 0.1, 1.4, [1.2, 1.3, 1.4]),
69
+ (1.2e-12, 0.1e-12, 1.4e-12, [1.2e-12, 1.3e-12, 1.4e-12]),
70
+ (1.4, -0.1, 1.2, [1.4, 1.3, 1.2]),
71
+ (1.02e-3, 0.01e-3, 1.04e-3, [0.00102, 0.00103, 0.00104]),
72
+ ],
73
+ )
74
+ def test_arange(start, step, stop, expected):
75
+ from hydraflow.executor.parser import _arange
76
+
77
+ np.testing.assert_allclose(_arange(start, step, stop), expected)
78
+
79
+
80
+ def test_arange_error():
81
+ from hydraflow.executor.parser import _arange
82
+
83
+ with pytest.raises(ValueError):
84
+ _arange(1.0, 0.0, 1.0)
85
+
86
+
52
87
  @pytest.mark.parametrize(
53
88
  ("s", "x"),
54
89
  [
@@ -86,20 +121,27 @@ def test_split_suffix(s, x):
86
121
  ("1:M", ["1e6"]),
87
122
  ("0.234p", ["0.234p"]),
88
123
  ("1:3", ["1", "2", "3"]),
89
- ("0:0.25:1", ["0", "0.25", "0.5", "0.75", "1.0"]),
124
+ ("0:0.25:1", ["0", "0.25", "0.5", "0.75", "1"]),
125
+ ("10:0.25:11", ["10", "10.25", "10.5", "10.75", "11"]),
90
126
  (":3", ["0", "1", "2", "3"]),
91
127
  ("5:7", ["5", "6", "7"]),
92
128
  ("-1:1", ["-1", "0", "1"]),
93
- ("1:0.5:2", ["1", "1.5", "2.0"]),
94
- ("1.:0.5:2", ["1.0", "1.5", "2.0"]),
95
- ("2:0.5:3", ["2", "2.5", "3.0"]),
96
- ("-1:0.5:1", ["-1", "-0.5", "0.0", "0.5", "1.0"]),
129
+ ("1:0.5:2", ["1", "1.5", "2"]),
130
+ ("1.:0.5:2", ["1", "1.5", "2"]),
131
+ ("2:0.5:3", ["2", "2.5", "3"]),
132
+ ("-1:0.5:1", ["-1", "-0.5", "0", "0.5", "1"]),
97
133
  ("4:-1:2", ["4", "3", "2"]),
98
- ("4.5:-1.5:2", ["4.5", "3.0"]),
99
- ("4.5:-1.5:1.5", ["4.5", "3.0", "1.5"]),
100
- ("4.5:-1.5:-4.5", ["4.5", "3.0", "1.5", "0.0", "-1.5", "-3.0", "-4.5"]),
134
+ ("4.5:-1.5:2", ["4.5", "3"]),
135
+ ("4.5:-1.5:1.5", ["4.5", "3", "1.5"]),
136
+ ("4.5:-1.5:-4.5", ["4.5", "3", "1.5", "0", "-1.5", "-3", "-4.5"]),
101
137
  ("1:2:u", ["1e-6", "2e-6"]),
102
- ("1:.25:2:n", ["1e-9", "1.25e-9", "1.5e-9", "1.75e-9", "2.0e-9"]),
138
+ ("1:.25:2:n", ["1e-9", "1.25e-9", "1.5e-9", "1.75e-9", "2e-9"]),
139
+ ("1.2:0.1:1.4:k", ["1.2e3", "1.3e3", "1.4e3"]),
140
+ ("1.2e-3:0.1e-3:1.4e-3", ["0.0012", "0.0013", "0.0014"]),
141
+ ("1.2e-12:0.1e-12:1.4e-12", ["1.2e-12", "1.3e-12", "1.4e-12"]),
142
+ ("1.22e-6:0.01e-6:1.24e-6", ["1.22e-06", "1.23e-06", "1.24e-06"]),
143
+ ("1.2e3:0.1e3:1.4e3", ["1.2e+03", "1.3e+03", "1.4e+03"]),
144
+ ("1.2e6:0.1e6:1.4e6", ["1.2e+06", "1.3e+06", "1.4e+06"]),
103
145
  ("1:2:e2", ["1e2", "2e2"]),
104
146
  (":2:e2", ["0", "1e2", "2e2"]),
105
147
  ("-2:2:k", ["-2e3", "-1e3", "0", "1e3", "2e3"]),
@@ -117,7 +159,7 @@ def test_collect_value(s, x):
117
159
  [
118
160
  ("1,2,3", ["1", "2", "3"]),
119
161
  ("1:3,5:6", ["1", "2", "3", "5", "6"]),
120
- ("0:0.25:1,2.0", ["0", "0.25", "0.5", "0.75", "1.0", "2.0"]),
162
+ ("0:0.25:1,2.0", ["0", "0.25", "0.5", "0.75", "1", "2.0"]),
121
163
  ("3", ["3"]),
122
164
  ("3:k", ["3e3"]),
123
165
  ("1:3:k,3:2:7:M", ["1e3", "2e3", "3e3", "3e6", "5e6", "7e6"]),
@@ -140,7 +182,7 @@ def test_expand_value(s, x):
140
182
  [
141
183
  ("1,2,3", ["1e3", "2e3", "3e3"]),
142
184
  ("1:3,5:6", ["1e3", "2e3", "3e3", "5e3", "6e3"]),
143
- ("0:0.25:1,2.0", ["0e3", "0.25e3", "0.5e3", "0.75e3", "1.0e3", "2.0e3"]),
185
+ ("0:0.25:1,2.0", ["0e3", "0.25e3", "0.5e3", "0.75e3", "1e3", "2.0e3"]),
144
186
  ("3", ["3e3"]),
145
187
  ],
146
188
  )
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