paramflow 0.6__tar.gz → 0.6.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.
- {paramflow-0.6/paramflow.egg-info → paramflow-0.6.2}/PKG-INFO +30 -9
- paramflow-0.6.2/examples/print_params.py +13 -0
- paramflow-0.6.2/examples/usage/app.py +3 -0
- paramflow-0.6.2/paramflow/__pycache__/params.cpython-312.pyc +0 -0
- paramflow-0.6.2/paramflow/__pycache__/params.cpython-313.pyc +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/parser.cpython-312.pyc +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/parser.cpython-313.pyc +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow/params.py +11 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow/parser.py +10 -3
- {paramflow-0.6 → paramflow-0.6.2/paramflow.egg-info}/PKG-INFO +30 -9
- {paramflow-0.6 → paramflow-0.6.2}/paramflow.egg-info/SOURCES.txt +6 -3
- paramflow-0.6.2/paramflow.egg-info/top_level.txt +5 -0
- paramflow-0.6.2/pyproject.toml +30 -0
- paramflow-0.6.2/tests/convert_test.py +55 -0
- paramflow-0.6.2/tests/frozen_test.py +98 -0
- paramflow-0.6.2/tests/params_test.py +471 -0
- paramflow-0.6/MANIFEST.in +0 -3
- paramflow-0.6/paramflow/__pycache__/params.cpython-312.pyc +0 -0
- paramflow-0.6/paramflow/__pycache__/params.cpython-313.pyc +0 -0
- paramflow-0.6/paramflow.egg-info/top_level.txt +0 -1
- paramflow-0.6/pyproject.toml +0 -3
- paramflow-0.6/setup.py +0 -22
- {paramflow-0.6 → paramflow-0.6.2}/LICENSE +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/README.md +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow/__init__.py +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/__init__.cpython-312.pyc +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/__init__.cpython-313.pyc +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/convert.cpython-312.pyc +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/convert.cpython-313.pyc +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/frozen.cpython-312.pyc +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/frozen.cpython-313.pyc +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/frozen_test.cpython-312-pytest-8.3.4.pyc +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/params_test.cpython-312-pytest-8.3.4.pyc +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow/convert.py +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow/frozen.py +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow.egg-info/dependency_links.txt +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/paramflow.egg-info/requires.txt +0 -0
- {paramflow-0.6 → paramflow-0.6.2}/setup.cfg +0 -0
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: paramflow
|
|
3
|
-
Version: 0.6
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: A lightweight library for hyperparameter and configuration management
|
|
5
|
-
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2025 mduszyk
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: Homepage, https://github.com/mduszyk/paramflow
|
|
28
|
+
Project-URL: Repository, https://github.com/mduszyk/paramflow
|
|
6
29
|
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
7
32
|
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
34
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
35
|
+
Requires-Python: >=3.11
|
|
8
36
|
Description-Content-Type: text/markdown
|
|
9
37
|
License-File: LICENSE
|
|
10
38
|
Requires-Dist: pyyaml
|
|
11
39
|
Provides-Extra: dotenv
|
|
12
40
|
Requires-Dist: python-dotenv; extra == "dotenv"
|
|
13
|
-
Dynamic: classifier
|
|
14
|
-
Dynamic: description
|
|
15
|
-
Dynamic: description-content-type
|
|
16
|
-
Dynamic: home-page
|
|
17
41
|
Dynamic: license-file
|
|
18
|
-
Dynamic: provides-extra
|
|
19
|
-
Dynamic: requires-dist
|
|
20
|
-
Dynamic: summary
|
|
21
42
|
|
|
22
43
|
# paramflow
|
|
23
44
|
ParamFlow is a lightweight library for layered configuration management, tailored for machine learning projects and any application that needs to merge parameters from multiple sources. It merges files, environment variables, and CLI arguments in a defined order, activates named profiles, and returns a read-only, attribute-accessible dictionary.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import paramflow as pf
|
|
3
|
+
|
|
4
|
+
# Example usages:
|
|
5
|
+
# P_FILE=params.toml python print_params.py
|
|
6
|
+
# P_FILE=params.yaml P_PROFILE=prod python print_params.py
|
|
7
|
+
# python print_params.py --sources params.yaml --profile=prod
|
|
8
|
+
# python print_params.py --sources params.toml
|
|
9
|
+
# python print_params.py --sources dqn_train.toml --profile dqn-adam
|
|
10
|
+
# python print_params.py --sources dqn_train.toml --profile dqn-adam --batch_size 64
|
|
11
|
+
|
|
12
|
+
params = pf.load()
|
|
13
|
+
print(json.dumps(params, indent=4))
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -34,6 +34,14 @@ def load(*sources: str | dict,
|
|
|
34
34
|
:return: read-only parameters as frozen dict
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
+
for source in sources:
|
|
38
|
+
if not isinstance(source, (str, dict)):
|
|
39
|
+
raise TypeError(f"sources must be file paths or dicts, got {type(source).__name__}")
|
|
40
|
+
if not default_profile:
|
|
41
|
+
raise ValueError("default_profile must be a non-empty string")
|
|
42
|
+
if not profile_key:
|
|
43
|
+
raise ValueError("profile_key must be a non-empty string")
|
|
44
|
+
|
|
37
45
|
logger.debug('Reading meta params layer %d, source: %s', 0, 'pf.load')
|
|
38
46
|
meta = {
|
|
39
47
|
'sources': sources,
|
|
@@ -106,6 +114,9 @@ def activate_profile(params: Dict[str, Any], default_profile: str, profile: str)
|
|
|
106
114
|
profile_params['__source__'] = params['__source__']
|
|
107
115
|
profile_params['__profile__'] = [default_profile]
|
|
108
116
|
if profile is not None and profile != default_profile:
|
|
117
|
+
if profile not in params:
|
|
118
|
+
available = [k for k in params if not k.startswith('__') and k != default_profile]
|
|
119
|
+
raise ValueError(f"profile '{profile}' not found, available profiles: {available}")
|
|
109
120
|
active_profile_params = params[profile]
|
|
110
121
|
deep_merge(profile_params, active_profile_params)
|
|
111
122
|
profile_params['__profile__'].append(profile)
|
|
@@ -57,8 +57,9 @@ class YamlParser(Parser):
|
|
|
57
57
|
def __call__(self, *args) -> Dict[str, Any]:
|
|
58
58
|
with open(self.path, 'r') as fp:
|
|
59
59
|
params = yaml.safe_load(fp)
|
|
60
|
-
if
|
|
61
|
-
|
|
60
|
+
if not params:
|
|
61
|
+
return {}
|
|
62
|
+
params['__source__'] = [self.path]
|
|
62
63
|
return params
|
|
63
64
|
|
|
64
65
|
|
|
@@ -98,7 +99,13 @@ class DotEnvParser(Parser):
|
|
|
98
99
|
self.target_profile = target_profile
|
|
99
100
|
|
|
100
101
|
def __call__(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
101
|
-
|
|
102
|
+
try:
|
|
103
|
+
from dotenv import dotenv_values
|
|
104
|
+
except ImportError:
|
|
105
|
+
raise ImportError(
|
|
106
|
+
f"loading '{self.path}' requires dotenv support: "
|
|
107
|
+
"pip install 'paramflow[dotenv]'"
|
|
108
|
+
)
|
|
102
109
|
if self.target_profile is None and self.default_profile in params:
|
|
103
110
|
self.target_profile = self.default_profile
|
|
104
111
|
params: Dict[str, Any] = params.get(self.default_profile, params)
|
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: paramflow
|
|
3
|
-
Version: 0.6
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: A lightweight library for hyperparameter and configuration management
|
|
5
|
-
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2025 mduszyk
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: Homepage, https://github.com/mduszyk/paramflow
|
|
28
|
+
Project-URL: Repository, https://github.com/mduszyk/paramflow
|
|
6
29
|
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
7
32
|
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
34
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
35
|
+
Requires-Python: >=3.11
|
|
8
36
|
Description-Content-Type: text/markdown
|
|
9
37
|
License-File: LICENSE
|
|
10
38
|
Requires-Dist: pyyaml
|
|
11
39
|
Provides-Extra: dotenv
|
|
12
40
|
Requires-Dist: python-dotenv; extra == "dotenv"
|
|
13
|
-
Dynamic: classifier
|
|
14
|
-
Dynamic: description
|
|
15
|
-
Dynamic: description-content-type
|
|
16
|
-
Dynamic: home-page
|
|
17
41
|
Dynamic: license-file
|
|
18
|
-
Dynamic: provides-extra
|
|
19
|
-
Dynamic: requires-dist
|
|
20
|
-
Dynamic: summary
|
|
21
42
|
|
|
22
43
|
# paramflow
|
|
23
44
|
ParamFlow is a lightweight library for layered configuration management, tailored for machine learning projects and any application that needs to merge parameters from multiple sources. It merges files, environment variables, and CLI arguments in a defined order, activates named profiles, and returns a read-only, attribute-accessible dictionary.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
LICENSE
|
|
2
|
-
MANIFEST.in
|
|
3
2
|
README.md
|
|
4
3
|
pyproject.toml
|
|
5
|
-
|
|
4
|
+
examples/print_params.py
|
|
5
|
+
examples/usage/app.py
|
|
6
6
|
paramflow/__init__.py
|
|
7
7
|
paramflow/convert.py
|
|
8
8
|
paramflow/frozen.py
|
|
@@ -24,4 +24,7 @@ paramflow/__pycache__/params.cpython-312.pyc
|
|
|
24
24
|
paramflow/__pycache__/params.cpython-313.pyc
|
|
25
25
|
paramflow/__pycache__/params_test.cpython-312-pytest-8.3.4.pyc
|
|
26
26
|
paramflow/__pycache__/parser.cpython-312.pyc
|
|
27
|
-
paramflow/__pycache__/parser.cpython-313.pyc
|
|
27
|
+
paramflow/__pycache__/parser.cpython-313.pyc
|
|
28
|
+
tests/convert_test.py
|
|
29
|
+
tests/frozen_test.py
|
|
30
|
+
tests/params_test.py
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "paramflow"
|
|
7
|
+
version = "0.6.2"
|
|
8
|
+
description = "A lightweight library for hyperparameter and configuration management"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
dependencies = ["pyyaml"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
dotenv = ["python-dotenv"]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/mduszyk/paramflow"
|
|
27
|
+
Repository = "https://github.com/mduszyk/paramflow"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["."]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from paramflow.convert import convert_type, infer_type
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_convert_type():
|
|
7
|
+
assert type(convert_type(3, 10)) is int
|
|
8
|
+
assert convert_type(3, 10) == 10
|
|
9
|
+
assert convert_type(3, '10') == 10
|
|
10
|
+
assert type(convert_type(3.0, 10)) is float
|
|
11
|
+
assert convert_type(3.0, 10) == 10.0
|
|
12
|
+
assert convert_type(3.14, '2.73') == 2.73
|
|
13
|
+
assert convert_type(False, 'true') == True
|
|
14
|
+
assert convert_type({}, '{"a": 1, "b": 2}') == {'a': 1, 'b': 2}
|
|
15
|
+
assert convert_type([], '[1, 2, 3]') == [1, 2, 3]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_failed_conversion():
|
|
19
|
+
with pytest.raises(TypeError):
|
|
20
|
+
convert_type({}, 5)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_infer_type():
|
|
24
|
+
assert infer_type('42') == 42
|
|
25
|
+
assert type(infer_type('42')) is int
|
|
26
|
+
assert infer_type('3.14') == 3.14
|
|
27
|
+
assert type(infer_type('3.14')) is float
|
|
28
|
+
assert infer_type('hello') == 'hello'
|
|
29
|
+
assert type(infer_type('hello')) is str
|
|
30
|
+
assert infer_type('true') is True
|
|
31
|
+
assert infer_type('false') is False
|
|
32
|
+
assert infer_type('True') is True
|
|
33
|
+
assert infer_type('False') is False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_convert_type_none_dst():
|
|
37
|
+
assert convert_type(None, 42) == 42
|
|
38
|
+
assert convert_type(None, 'hello') == 'hello'
|
|
39
|
+
assert convert_type(None, [1, 2, 3]) == [1, 2, 3]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_convert_type_bool_false():
|
|
43
|
+
assert convert_type(False, 'false') == False
|
|
44
|
+
assert convert_type(True, 'false') == False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_convert_type_str_to_tuple():
|
|
48
|
+
result = convert_type(('a',), 'hello')
|
|
49
|
+
assert result == ('hello',)
|
|
50
|
+
assert type(result) is tuple
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_convert_type_error_message_with_path():
|
|
54
|
+
with pytest.raises(TypeError, match='mykey'):
|
|
55
|
+
convert_type({}, 5, path='mykey')
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from paramflow.frozen import freeze, unfreeze, ParamsList, ParamsDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_freeze():
|
|
7
|
+
params = freeze({
|
|
8
|
+
'default': {
|
|
9
|
+
'name': 'test',
|
|
10
|
+
'lr': 1e-3,
|
|
11
|
+
'debug': True,
|
|
12
|
+
'list': [1, 2, 3],
|
|
13
|
+
'dict': {'foo': 1, 'bar': 2},
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
assert isinstance(params, ParamsDict)
|
|
17
|
+
assert params.default.name == 'test'
|
|
18
|
+
assert params.default.lr == 1e-3
|
|
19
|
+
assert params.default.debug
|
|
20
|
+
assert isinstance(params.default.list, ParamsList)
|
|
21
|
+
assert params.default.list[2] == 3
|
|
22
|
+
assert isinstance(params.default.dict, ParamsDict)
|
|
23
|
+
assert params.default.dict.foo == 1
|
|
24
|
+
assert params.default.dict.bar == 2
|
|
25
|
+
|
|
26
|
+
def test_unfreeze():
|
|
27
|
+
params = freeze({
|
|
28
|
+
'default': {
|
|
29
|
+
'name': 'test',
|
|
30
|
+
'list': [1, 2, 3],
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
params2 = freeze(unfreeze(params))
|
|
34
|
+
assert params.default.name == params2.default.name
|
|
35
|
+
assert len(params.default.list) == len(params2.default.list)
|
|
36
|
+
assert params.default.list[0] == params2.default.list[0]
|
|
37
|
+
assert params.default.list[1] == params2.default.list[1]
|
|
38
|
+
assert params.default.list[2] == params2.default.list[2]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_params_dict_immutability():
|
|
42
|
+
params = freeze({'x': 1, 'y': 2})
|
|
43
|
+
with pytest.raises(AttributeError):
|
|
44
|
+
params.x = 10
|
|
45
|
+
with pytest.raises(AttributeError):
|
|
46
|
+
del params.x
|
|
47
|
+
with pytest.raises(TypeError):
|
|
48
|
+
params['x'] = 10
|
|
49
|
+
with pytest.raises(TypeError):
|
|
50
|
+
del params['x']
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_params_dict_missing_key():
|
|
54
|
+
params = freeze({'x': 1})
|
|
55
|
+
with pytest.raises(AttributeError, match="has no param 'z'"):
|
|
56
|
+
_ = params.z
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_params_list_immutability():
|
|
60
|
+
pl = freeze([1, 2, 3])
|
|
61
|
+
with pytest.raises(TypeError):
|
|
62
|
+
pl[0] = 99
|
|
63
|
+
with pytest.raises(TypeError):
|
|
64
|
+
del pl[0]
|
|
65
|
+
with pytest.raises(TypeError):
|
|
66
|
+
pl.append(4)
|
|
67
|
+
with pytest.raises(TypeError):
|
|
68
|
+
pl.extend([4, 5])
|
|
69
|
+
with pytest.raises(TypeError):
|
|
70
|
+
pl.insert(0, 0)
|
|
71
|
+
with pytest.raises(TypeError):
|
|
72
|
+
pl.remove(1)
|
|
73
|
+
with pytest.raises(TypeError):
|
|
74
|
+
pl.pop()
|
|
75
|
+
with pytest.raises(TypeError):
|
|
76
|
+
pl.clear()
|
|
77
|
+
with pytest.raises(TypeError):
|
|
78
|
+
pl.__iadd__([4])
|
|
79
|
+
with pytest.raises(TypeError):
|
|
80
|
+
pl.__imul__(2)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_freeze_list():
|
|
84
|
+
frozen = freeze([1, {'a': 2}, [3, 4]])
|
|
85
|
+
assert isinstance(frozen, ParamsList)
|
|
86
|
+
assert frozen[0] == 1
|
|
87
|
+
assert isinstance(frozen[1], ParamsDict)
|
|
88
|
+
assert frozen[1].a == 2
|
|
89
|
+
assert isinstance(frozen[2], ParamsList)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_unfreeze_list():
|
|
93
|
+
frozen = freeze([1, {'a': 2}, [3, 4]])
|
|
94
|
+
unfrozen = unfreeze(frozen)
|
|
95
|
+
assert type(unfrozen) is list
|
|
96
|
+
assert unfrozen[0] == 1
|
|
97
|
+
assert type(unfrozen[1]) is dict
|
|
98
|
+
assert type(unfrozen[2]) is list
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from functools import reduce
|
|
4
|
+
from tempfile import NamedTemporaryFile
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
import paramflow as pf
|
|
9
|
+
from paramflow.params import activate_profile, deep_merge, build_parsers
|
|
10
|
+
from paramflow.parser import EnvParser, DictParser, DotEnvParser, get_env_params
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def temp_file(request):
|
|
15
|
+
def create_temp_file(content, suffix):
|
|
16
|
+
tmp = NamedTemporaryFile(delete=False, mode='w+', suffix=suffix)
|
|
17
|
+
tmp.write(content)
|
|
18
|
+
tmp.close()
|
|
19
|
+
request.addfinalizer(lambda: os.remove(tmp.name))
|
|
20
|
+
return tmp.name
|
|
21
|
+
return create_temp_file
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_deep_merge():
|
|
25
|
+
dst = {
|
|
26
|
+
'default': {
|
|
27
|
+
'name': 'test',
|
|
28
|
+
'lr': 1e-3,
|
|
29
|
+
'debug': True,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
src = {'default': {'name': 'test123'}}
|
|
33
|
+
deep_merge(dst, src)
|
|
34
|
+
assert dst['default']['name'] == 'test123'
|
|
35
|
+
|
|
36
|
+
def test_deep_merge_list():
|
|
37
|
+
dst = {'default': {'tags': ['a', 'b', 'c']}}
|
|
38
|
+
src = {'default': {'tags': ['x', 'y', 'z']}}
|
|
39
|
+
deep_merge(dst, src)
|
|
40
|
+
assert dst['default']['tags'] == ['x', 'y', 'z']
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_deep_merge_empty_dict():
|
|
44
|
+
dst = {
|
|
45
|
+
'default': {
|
|
46
|
+
'kwargs': {
|
|
47
|
+
'lr': 1e-3,
|
|
48
|
+
'debug': True,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
src = {'default': {'kwargs': {}}}
|
|
53
|
+
deep_merge(dst, src)
|
|
54
|
+
assert dst['default']['kwargs'] == {}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_activate_profile():
|
|
58
|
+
params = {
|
|
59
|
+
'default': { 'debug': True },
|
|
60
|
+
'prod': { 'debug': False }
|
|
61
|
+
}
|
|
62
|
+
params = activate_profile(params, 'default', 'prod')
|
|
63
|
+
assert not params['debug']
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_merge_override_layers():
|
|
67
|
+
params = {'default': { 'name': 'test' }}
|
|
68
|
+
overrides = {'default': {'name': 'test123'}}
|
|
69
|
+
params = reduce(deep_merge, [params, overrides])
|
|
70
|
+
params = activate_profile(params, 'default', 'default')
|
|
71
|
+
assert params['name'] == 'test123'
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_merge_multiple_layers_and_activate():
|
|
75
|
+
layer1 = {'default': {'debug': True, 'name': 'Joe', 'age': 20}}
|
|
76
|
+
layer2 = {'prod': { 'debug': False }}
|
|
77
|
+
layer3 = {'prod': {'name': 'Jane'}}
|
|
78
|
+
layer4 = {'prod': {'age': 30}}
|
|
79
|
+
layers = [layer1, layer2, layer3, layer4]
|
|
80
|
+
params = reduce(deep_merge, layers)
|
|
81
|
+
params = activate_profile(params, 'default', 'prod')
|
|
82
|
+
assert not params['debug']
|
|
83
|
+
assert params['name'] == 'Jane'
|
|
84
|
+
assert params['age'] == 30
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_toml_no_profiles(temp_file, monkeypatch):
|
|
88
|
+
file_content = (
|
|
89
|
+
"""
|
|
90
|
+
name = 'test'
|
|
91
|
+
lr = 1e-3
|
|
92
|
+
debug = true
|
|
93
|
+
"""
|
|
94
|
+
)
|
|
95
|
+
file_path = temp_file(file_content, '.toml')
|
|
96
|
+
monkeypatch.setattr(sys, 'argv', ['test.py'])
|
|
97
|
+
params = pf.load(file_path)
|
|
98
|
+
assert params.name == 'test'
|
|
99
|
+
assert params.lr == 1e-3
|
|
100
|
+
assert params.debug
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_toml_default(temp_file, monkeypatch):
|
|
104
|
+
file_content = (
|
|
105
|
+
"""
|
|
106
|
+
[default]
|
|
107
|
+
name = 'test'
|
|
108
|
+
lr = 1e-3
|
|
109
|
+
debug = true
|
|
110
|
+
"""
|
|
111
|
+
)
|
|
112
|
+
file_path = temp_file(file_content, '.toml')
|
|
113
|
+
monkeypatch.setattr(sys, 'argv', ['test.py'])
|
|
114
|
+
params = pf.load(file_path)
|
|
115
|
+
assert params.name == 'test'
|
|
116
|
+
assert params.lr == 1e-3
|
|
117
|
+
assert params.debug
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_yaml_profile_env_args(temp_file, monkeypatch):
|
|
121
|
+
file_content = (
|
|
122
|
+
"""
|
|
123
|
+
default:
|
|
124
|
+
name: 'dev'
|
|
125
|
+
lr: 0.001
|
|
126
|
+
debug: true
|
|
127
|
+
prod:
|
|
128
|
+
debug: false
|
|
129
|
+
"""
|
|
130
|
+
)
|
|
131
|
+
file_path = temp_file(file_content, '.yaml')
|
|
132
|
+
monkeypatch.setenv('P_LR', '0.0001')
|
|
133
|
+
monkeypatch.setattr(sys, 'argv', ['test.py', '--profile', 'prod', '--name', 'production'])
|
|
134
|
+
params = pf.load(file_path)
|
|
135
|
+
assert params.name == 'production'
|
|
136
|
+
assert params.lr == 1e-4
|
|
137
|
+
assert not params.debug
|
|
138
|
+
assert params.__source__ == [file_path, 'env', 'args']
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_load_all_layers(temp_file, monkeypatch):
|
|
142
|
+
file1 = temp_file("default:\n lr: 0.001", '.yaml')
|
|
143
|
+
file2 = temp_file("[default]\nname = 'dev'", '.ini')
|
|
144
|
+
file3 = temp_file('[default]\ndebug = true\nbatch_size=32', '.toml')
|
|
145
|
+
file4 = temp_file('{"prod": {"debug": false}}', '.json')
|
|
146
|
+
file5 = temp_file('P_NAME=production', '.env')
|
|
147
|
+
monkeypatch.setenv('P_LR', '0.0001')
|
|
148
|
+
monkeypatch.setenv('P_PROFILE', 'prod')
|
|
149
|
+
monkeypatch.setattr(sys, 'argv', ['test.py', '--batch_size', '64'])
|
|
150
|
+
params = pf.load(file1, file2, file3, file4, file5)
|
|
151
|
+
assert params.name == 'production'
|
|
152
|
+
assert params.lr == 0.0001
|
|
153
|
+
assert params.batch_size == 64
|
|
154
|
+
assert not params.debug
|
|
155
|
+
assert params.__source__ == [file1, file2, file3, file4, file5, 'env', 'args']
|
|
156
|
+
assert params.__profile__ == ['default', 'prod']
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_custom_merge_order(temp_file, monkeypatch):
|
|
160
|
+
file_toml = temp_file('[default]\nname = "local"\ndebug = true\nbatch_size=32', '.toml')
|
|
161
|
+
dot_env = temp_file('P_NAME=prod', '.env')
|
|
162
|
+
monkeypatch.setenv('P_NAME', 'dev')
|
|
163
|
+
monkeypatch.setattr(sys, 'argv', ['test.py', '--batch_size', '64'])
|
|
164
|
+
source = [file_toml, 'env', dot_env, 'args']
|
|
165
|
+
params = pf.load(*source)
|
|
166
|
+
assert params.name == 'prod'
|
|
167
|
+
assert params.batch_size == 64
|
|
168
|
+
assert params.debug
|
|
169
|
+
assert params.__source__ == source
|
|
170
|
+
assert params.__profile__ == ['default']
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_specify_file_via_cmd(temp_file, monkeypatch):
|
|
174
|
+
file_toml = temp_file('[default]\nname = "dev"\ndebug = true\nbatch_size=32', '.toml')
|
|
175
|
+
monkeypatch.setattr(sys, 'argv', ['test.py', '--sources', file_toml])
|
|
176
|
+
params = pf.load()
|
|
177
|
+
assert params.name == 'dev'
|
|
178
|
+
assert params.batch_size == 32
|
|
179
|
+
assert params.debug
|
|
180
|
+
assert params.__source__ == [file_toml]
|
|
181
|
+
assert params.__profile__ == ['default']
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_nested_configuration(temp_file):
|
|
185
|
+
file1_yaml = temp_file(
|
|
186
|
+
"""
|
|
187
|
+
default:
|
|
188
|
+
level1:
|
|
189
|
+
name: 'abc'
|
|
190
|
+
value: 17
|
|
191
|
+
level2:
|
|
192
|
+
name: 'foo'
|
|
193
|
+
value: 0
|
|
194
|
+
""", '.yaml')
|
|
195
|
+
file2_yaml = temp_file(
|
|
196
|
+
"""
|
|
197
|
+
default:
|
|
198
|
+
level1:
|
|
199
|
+
name: 'bar'
|
|
200
|
+
level2:
|
|
201
|
+
value: 42
|
|
202
|
+
""", '.yaml')
|
|
203
|
+
params = pf.load(file1_yaml, file2_yaml)
|
|
204
|
+
assert params.level1.name == 'bar'
|
|
205
|
+
assert params.level1.value == 17
|
|
206
|
+
assert params.level1.level2.name == 'foo'
|
|
207
|
+
assert params.level1.level2.value == 42
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_args_only_param(temp_file, monkeypatch):
|
|
211
|
+
file_content = (
|
|
212
|
+
"""
|
|
213
|
+
[default]
|
|
214
|
+
lr = 1e-3
|
|
215
|
+
debug = true
|
|
216
|
+
"""
|
|
217
|
+
)
|
|
218
|
+
file_path = temp_file(file_content, '.toml')
|
|
219
|
+
monkeypatch.setattr(sys, 'argv', ['test.py', '--batch_size', '64', '--name', 'test'])
|
|
220
|
+
params = pf.load(file_path)
|
|
221
|
+
assert params.lr == 1e-3
|
|
222
|
+
assert params.debug
|
|
223
|
+
assert params.batch_size == 64
|
|
224
|
+
assert params.name == 'test'
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_dict_params(temp_file, monkeypatch):
|
|
228
|
+
file_content = (
|
|
229
|
+
"""
|
|
230
|
+
[default]
|
|
231
|
+
lr = 1e-3
|
|
232
|
+
debug = true
|
|
233
|
+
"""
|
|
234
|
+
)
|
|
235
|
+
file_path = temp_file(file_content, '.toml')
|
|
236
|
+
monkeypatch.setattr(sys, 'argv', ['test.py'])
|
|
237
|
+
params = pf.load(file_path, {'name': 'test'})
|
|
238
|
+
assert params.lr == 1e-3
|
|
239
|
+
assert params.debug
|
|
240
|
+
assert params.name == 'test'
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_load_invalid_source_type(monkeypatch):
|
|
244
|
+
monkeypatch.setattr(sys, 'argv', ['test.py'])
|
|
245
|
+
with pytest.raises(TypeError, match='int'):
|
|
246
|
+
pf.load(42)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_load_invalid_default_profile(temp_file, monkeypatch):
|
|
250
|
+
monkeypatch.setattr(sys, 'argv', ['test.py'])
|
|
251
|
+
with pytest.raises(ValueError, match='default_profile'):
|
|
252
|
+
pf.load(default_profile='')
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_load_invalid_profile_key(temp_file, monkeypatch):
|
|
256
|
+
monkeypatch.setattr(sys, 'argv', ['test.py'])
|
|
257
|
+
with pytest.raises(ValueError, match='profile_key'):
|
|
258
|
+
pf.load(profile_key='')
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_deep_merge_source_key():
|
|
262
|
+
dst = {'__source__': ['a']}
|
|
263
|
+
src = {'__source__': ['b', 'c']}
|
|
264
|
+
deep_merge(dst, src)
|
|
265
|
+
assert dst['__source__'] == ['a', 'b', 'c']
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_deep_merge_list_length_mismatch():
|
|
269
|
+
dst = {'default': {'tags': ['a', 'b']}}
|
|
270
|
+
src = {'default': {'tags': ['x', 'y', 'z']}}
|
|
271
|
+
deep_merge(dst, src)
|
|
272
|
+
assert dst['default']['tags'] == ['x', 'y', 'z']
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_deep_merge_list_of_dicts():
|
|
276
|
+
dst = {'items': [{'name': 'a', 'val': 1}, {'name': 'b', 'val': 2}]}
|
|
277
|
+
src = {'items': [{'val': 10}, {'name': 'B'}]}
|
|
278
|
+
deep_merge(dst, src)
|
|
279
|
+
assert dst['items'][0]['name'] == 'a'
|
|
280
|
+
assert dst['items'][0]['val'] == 10
|
|
281
|
+
assert dst['items'][1]['name'] == 'B'
|
|
282
|
+
assert dst['items'][1]['val'] == 2
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_activate_profile_missing():
|
|
286
|
+
params = {'default': {'x': 1}, 'prod': {'x': 2}}
|
|
287
|
+
with pytest.raises(ValueError, match="profile 'staging' not found"):
|
|
288
|
+
activate_profile(params, 'default', 'staging')
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_activate_profile_missing_lists_available():
|
|
292
|
+
params = {'default': {'x': 1}, 'prod': {'x': 2}, 'dev': {'x': 3}, '__source__': ['f.toml']}
|
|
293
|
+
with pytest.raises(ValueError) as exc:
|
|
294
|
+
activate_profile(params, 'default', 'staging')
|
|
295
|
+
msg = str(exc.value)
|
|
296
|
+
assert 'prod' in msg
|
|
297
|
+
assert 'dev' in msg
|
|
298
|
+
assert 'default' not in msg
|
|
299
|
+
assert '__source__' not in msg
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def test_activate_profile_none():
|
|
303
|
+
params = {'default': {'x': 1}, 'prod': {'x': 2}}
|
|
304
|
+
result = activate_profile(params, 'default', None)
|
|
305
|
+
assert result['x'] == 1
|
|
306
|
+
assert result['__profile__'] == ['default']
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_activate_profile_same_as_default():
|
|
310
|
+
params = {'default': {'x': 1}}
|
|
311
|
+
result = activate_profile(params, 'default', 'default')
|
|
312
|
+
assert result['x'] == 1
|
|
313
|
+
assert result['__profile__'] == ['default']
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def test_activate_profile_no_profile_key():
|
|
317
|
+
params = {'x': 1, 'y': 2}
|
|
318
|
+
result = activate_profile(params, 'default', None)
|
|
319
|
+
assert result['x'] == 1
|
|
320
|
+
assert result['__profile__'] == ['default']
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_activate_profile_source_propagation():
|
|
324
|
+
params = {
|
|
325
|
+
'default': {'x': 1},
|
|
326
|
+
'__source__': ['file.toml'],
|
|
327
|
+
}
|
|
328
|
+
result = activate_profile(params, 'default', None)
|
|
329
|
+
assert result['__source__'] == ['file.toml']
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def test_load_with_profile_kwarg(temp_file, monkeypatch):
|
|
333
|
+
file_content = """
|
|
334
|
+
[default]
|
|
335
|
+
debug = true
|
|
336
|
+
[prod]
|
|
337
|
+
debug = false
|
|
338
|
+
"""
|
|
339
|
+
file_path = temp_file(file_content, '.toml')
|
|
340
|
+
monkeypatch.setattr(sys, 'argv', ['test.py'])
|
|
341
|
+
params = pf.load(file_path, profile='prod')
|
|
342
|
+
assert not params.debug
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_get_env_params_direct():
|
|
346
|
+
env = {'P_NAME': 'alice', 'P_LR': '0.01', 'OTHER': 'ignored'}
|
|
347
|
+
ref_params = {'name': 'bob', 'lr': 0.001}
|
|
348
|
+
result = get_env_params(env, 'P_', ref_params)
|
|
349
|
+
assert result == {'name': 'alice', 'lr': '0.01'}
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def test_env_parser_no_match():
|
|
353
|
+
parser = EnvParser('ZZZNOMATCH_', 'default')
|
|
354
|
+
result = parser({'default': {'name': 'test', 'lr': 0.001}})
|
|
355
|
+
assert result == {}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def test_dict_parser_direct():
|
|
359
|
+
data = {'name': 'test', 'lr': 0.001}
|
|
360
|
+
parser = DictParser(data)
|
|
361
|
+
result = parser()
|
|
362
|
+
assert result['default'] == data
|
|
363
|
+
assert result['__source__'] == [data]
|
|
364
|
+
assert result['default'] is not data
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def test_build_parsers_unknown_extension():
|
|
368
|
+
meta = pf.freeze({
|
|
369
|
+
'env_prefix': 'P_',
|
|
370
|
+
'args_prefix': '',
|
|
371
|
+
'default_profile': 'default',
|
|
372
|
+
'profile': None,
|
|
373
|
+
})
|
|
374
|
+
with pytest.raises(ValueError, match=r"unsupported file format '\.xyz'"):
|
|
375
|
+
build_parsers(['config.xyz'], meta)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def test_yaml_empty_file(temp_file, monkeypatch):
|
|
379
|
+
file_path = temp_file('', '.yaml')
|
|
380
|
+
monkeypatch.setattr(sys, 'argv', ['test.py'])
|
|
381
|
+
params = pf.load(file_path)
|
|
382
|
+
assert params.__profile__ == ['default']
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_yaml_comments_only(temp_file, monkeypatch):
|
|
386
|
+
file_path = temp_file('# just a comment\n', '.yaml')
|
|
387
|
+
monkeypatch.setattr(sys, 'argv', ['test.py'])
|
|
388
|
+
params = pf.load(file_path)
|
|
389
|
+
assert params.__profile__ == ['default']
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def test_dotenv_parser_missing_dependency(temp_file, monkeypatch):
|
|
393
|
+
dot_env = temp_file('P_NAME=alice', '.env')
|
|
394
|
+
monkeypatch.setitem(sys.modules, 'dotenv', None)
|
|
395
|
+
parser = DotEnvParser(dot_env, 'P_', 'default')
|
|
396
|
+
with pytest.raises(ImportError, match="pip install 'paramflow\\[dotenv\\]'"):
|
|
397
|
+
parser({'default': {'name': 'bob'}})
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def test_dotenv_parser_basic(temp_file):
|
|
401
|
+
dot_env = temp_file('P_NAME=alice\nP_LR=0.01\nOTHER=ignored', '.env')
|
|
402
|
+
parser = DotEnvParser(dot_env, 'P_', 'default')
|
|
403
|
+
result = parser({'default': {'name': 'bob', 'lr': 0.001}})
|
|
404
|
+
assert result['default']['name'] == 'alice'
|
|
405
|
+
assert result['default']['lr'] == '0.01'
|
|
406
|
+
assert result['__source__'] == [dot_env]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def test_dotenv_parser_prefix_filter(temp_file):
|
|
410
|
+
dot_env = temp_file('P_NAME=alice\nOTHER_NAME=bob', '.env')
|
|
411
|
+
parser = DotEnvParser(dot_env, 'P_', 'default')
|
|
412
|
+
result = parser({'default': {'name': 'default_name'}})
|
|
413
|
+
assert result['default']['name'] == 'alice'
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def test_dotenv_parser_key_filter(temp_file):
|
|
417
|
+
dot_env = temp_file('P_NAME=alice\nP_UNKNOWN=xyz', '.env')
|
|
418
|
+
parser = DotEnvParser(dot_env, 'P_', 'default')
|
|
419
|
+
result = parser({'default': {'name': 'bob'}})
|
|
420
|
+
assert result['default'] == {'name': 'alice'}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def test_dotenv_parser_no_match(temp_file):
|
|
424
|
+
dot_env = temp_file('OTHER=value', '.env')
|
|
425
|
+
parser = DotEnvParser(dot_env, 'P_', 'default')
|
|
426
|
+
result = parser({'default': {'name': 'bob'}})
|
|
427
|
+
assert result == {}
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def test_dotenv_parser_target_profile(temp_file):
|
|
431
|
+
dot_env = temp_file('P_NAME=alice', '.env')
|
|
432
|
+
parser = DotEnvParser(dot_env, 'P_', 'default', target_profile='prod')
|
|
433
|
+
result = parser({'default': {'name': 'bob'}})
|
|
434
|
+
assert result['prod']['name'] == 'alice'
|
|
435
|
+
assert result['__source__'] == [dot_env]
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def test_load_dotenv_overrides(temp_file, monkeypatch):
|
|
439
|
+
toml = temp_file('[default]\nname = "dev"\nlr = 0.001', '.toml')
|
|
440
|
+
dot_env = temp_file('P_NAME=production\nP_LR=0.0001', '.env')
|
|
441
|
+
monkeypatch.setattr(sys, 'argv', ['test.py'])
|
|
442
|
+
params = pf.load(toml, dot_env)
|
|
443
|
+
assert params.name == 'production'
|
|
444
|
+
assert params.lr == 0.0001
|
|
445
|
+
assert dot_env in params.__source__
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def test_load_dotenv_type_conversion(temp_file, monkeypatch):
|
|
449
|
+
toml = temp_file('[default]\ndebug = true\nbatch_size = 32', '.toml')
|
|
450
|
+
dot_env = temp_file('P_DEBUG=false\nP_BATCH_SIZE=64', '.env')
|
|
451
|
+
monkeypatch.setattr(sys, 'argv', ['test.py'])
|
|
452
|
+
params = pf.load(toml, dot_env)
|
|
453
|
+
assert params.debug == False
|
|
454
|
+
assert params.batch_size == 64
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def test_help(temp_file, monkeypatch, capsys):
|
|
458
|
+
file_content = (
|
|
459
|
+
"""
|
|
460
|
+
[default]
|
|
461
|
+
lr = 1e-3
|
|
462
|
+
debug = true
|
|
463
|
+
"""
|
|
464
|
+
)
|
|
465
|
+
file_path = temp_file(file_content, '.toml')
|
|
466
|
+
monkeypatch.setattr(sys, 'argv', ['test.py', '--help'])
|
|
467
|
+
with pytest.raises(SystemExit) as exc:
|
|
468
|
+
pf.load(file_path)
|
|
469
|
+
captured = capsys.readouterr()
|
|
470
|
+
assert 'Meta-parameters' in captured.out
|
|
471
|
+
assert 'Parameters' in captured.out
|
paramflow-0.6/MANIFEST.in
DELETED
|
Binary file
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
paramflow
|
paramflow-0.6/pyproject.toml
DELETED
paramflow-0.6/setup.py
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
from setuptools import setup, find_packages
|
|
2
|
-
|
|
3
|
-
setup(
|
|
4
|
-
name='paramflow',
|
|
5
|
-
version='0.6',
|
|
6
|
-
description='A lightweight library for hyperparameter and configuration management',
|
|
7
|
-
packages=find_packages(),
|
|
8
|
-
install_requires=[
|
|
9
|
-
"pyyaml",
|
|
10
|
-
],
|
|
11
|
-
extras_require={
|
|
12
|
-
"dotenv": ["python-dotenv"],
|
|
13
|
-
},
|
|
14
|
-
entry_points={},
|
|
15
|
-
long_description=open('README.md').read(),
|
|
16
|
-
long_description_content_type='text/markdown',
|
|
17
|
-
url='https://github.com/mduszyk/paramflow',
|
|
18
|
-
classifiers=[
|
|
19
|
-
'Programming Language :: Python :: 3',
|
|
20
|
-
'License :: OSI Approved :: MIT License',
|
|
21
|
-
],
|
|
22
|
-
)
|
|
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
|
{paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/frozen_test.cpython-312-pytest-8.3.4.pyc
RENAMED
|
File without changes
|
{paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/params_test.cpython-312-pytest-8.3.4.pyc
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|