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.
Files changed (38) hide show
  1. {paramflow-0.6/paramflow.egg-info → paramflow-0.6.2}/PKG-INFO +30 -9
  2. paramflow-0.6.2/examples/print_params.py +13 -0
  3. paramflow-0.6.2/examples/usage/app.py +3 -0
  4. paramflow-0.6.2/paramflow/__pycache__/params.cpython-312.pyc +0 -0
  5. paramflow-0.6.2/paramflow/__pycache__/params.cpython-313.pyc +0 -0
  6. {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/parser.cpython-312.pyc +0 -0
  7. {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/parser.cpython-313.pyc +0 -0
  8. {paramflow-0.6 → paramflow-0.6.2}/paramflow/params.py +11 -0
  9. {paramflow-0.6 → paramflow-0.6.2}/paramflow/parser.py +10 -3
  10. {paramflow-0.6 → paramflow-0.6.2/paramflow.egg-info}/PKG-INFO +30 -9
  11. {paramflow-0.6 → paramflow-0.6.2}/paramflow.egg-info/SOURCES.txt +6 -3
  12. paramflow-0.6.2/paramflow.egg-info/top_level.txt +5 -0
  13. paramflow-0.6.2/pyproject.toml +30 -0
  14. paramflow-0.6.2/tests/convert_test.py +55 -0
  15. paramflow-0.6.2/tests/frozen_test.py +98 -0
  16. paramflow-0.6.2/tests/params_test.py +471 -0
  17. paramflow-0.6/MANIFEST.in +0 -3
  18. paramflow-0.6/paramflow/__pycache__/params.cpython-312.pyc +0 -0
  19. paramflow-0.6/paramflow/__pycache__/params.cpython-313.pyc +0 -0
  20. paramflow-0.6/paramflow.egg-info/top_level.txt +0 -1
  21. paramflow-0.6/pyproject.toml +0 -3
  22. paramflow-0.6/setup.py +0 -22
  23. {paramflow-0.6 → paramflow-0.6.2}/LICENSE +0 -0
  24. {paramflow-0.6 → paramflow-0.6.2}/README.md +0 -0
  25. {paramflow-0.6 → paramflow-0.6.2}/paramflow/__init__.py +0 -0
  26. {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/__init__.cpython-312.pyc +0 -0
  27. {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/__init__.cpython-313.pyc +0 -0
  28. {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/convert.cpython-312.pyc +0 -0
  29. {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/convert.cpython-313.pyc +0 -0
  30. {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/frozen.cpython-312.pyc +0 -0
  31. {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/frozen.cpython-313.pyc +0 -0
  32. {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/frozen_test.cpython-312-pytest-8.3.4.pyc +0 -0
  33. {paramflow-0.6 → paramflow-0.6.2}/paramflow/__pycache__/params_test.cpython-312-pytest-8.3.4.pyc +0 -0
  34. {paramflow-0.6 → paramflow-0.6.2}/paramflow/convert.py +0 -0
  35. {paramflow-0.6 → paramflow-0.6.2}/paramflow/frozen.py +0 -0
  36. {paramflow-0.6 → paramflow-0.6.2}/paramflow.egg-info/dependency_links.txt +0 -0
  37. {paramflow-0.6 → paramflow-0.6.2}/paramflow.egg-info/requires.txt +0 -0
  38. {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
- Home-page: https://github.com/mduszyk/paramflow
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))
@@ -0,0 +1,3 @@
1
+ import paramflow as pf
2
+ params = pf.load('params.toml')
3
+ print(params.learning_rate)
@@ -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 len(params) > 0:
61
- params['__source__'] = [self.path]
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
- from dotenv import dotenv_values
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
- Home-page: https://github.com/mduszyk/paramflow
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
- setup.py
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,5 @@
1
+ build
2
+ dist
3
+ examples
4
+ paramflow
5
+ tests
@@ -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
@@ -1,3 +0,0 @@
1
- include README.md
2
- include LICENSE
3
- recursive-include paramflow *
@@ -1 +0,0 @@
1
- paramflow
@@ -1,3 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools", "wheel"]
3
- build-backend = "setuptools.build_meta"
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