configaroo 0.2.0__tar.gz → 0.2.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.
- {configaroo-0.2.0 → configaroo-0.2.2}/LICENSE +1 -1
- {configaroo-0.2.0 → configaroo-0.2.2}/PKG-INFO +1 -1
- {configaroo-0.2.0 → configaroo-0.2.2}/README.md +1 -1
- {configaroo-0.2.0 → configaroo-0.2.2}/pyproject.toml +3 -2
- configaroo-0.2.2/src/configaroo/__init__.py +17 -0
- {configaroo-0.2.0 → configaroo-0.2.2}/src/configaroo/configuration.py +26 -7
- {configaroo-0.2.0 → configaroo-0.2.2}/src/configaroo.egg-info/PKG-INFO +1 -1
- {configaroo-0.2.0 → configaroo-0.2.2}/src/configaroo.egg-info/SOURCES.txt +1 -0
- {configaroo-0.2.0 → configaroo-0.2.2}/tests/test_configuration.py +25 -48
- configaroo-0.2.2/tests/test_dynamic.py +69 -0
- {configaroo-0.2.0 → configaroo-0.2.2}/tests/test_validation.py +6 -0
- configaroo-0.2.0/src/configaroo/__init__.py +0 -10
- {configaroo-0.2.0 → configaroo-0.2.2}/setup.cfg +0 -0
- {configaroo-0.2.0 → configaroo-0.2.2}/src/configaroo/exceptions.py +0 -0
- {configaroo-0.2.0 → configaroo-0.2.2}/src/configaroo/loaders/__init__.py +0 -0
- {configaroo-0.2.0 → configaroo-0.2.2}/src/configaroo/loaders/json.py +0 -0
- {configaroo-0.2.0 → configaroo-0.2.2}/src/configaroo/loaders/toml.py +0 -0
- {configaroo-0.2.0 → configaroo-0.2.2}/src/configaroo/py.typed +0 -0
- {configaroo-0.2.0 → configaroo-0.2.2}/src/configaroo.egg-info/dependency_links.txt +0 -0
- {configaroo-0.2.0 → configaroo-0.2.2}/src/configaroo.egg-info/requires.txt +0 -0
- {configaroo-0.2.0 → configaroo-0.2.2}/src/configaroo.egg-info/top_level.txt +0 -0
- {configaroo-0.2.0 → configaroo-0.2.2}/tests/test_environment.py +0 -0
- {configaroo-0.2.0 → configaroo-0.2.2}/tests/test_json.py +0 -0
- {configaroo-0.2.0 → configaroo-0.2.2}/tests/test_loaders.py +0 -0
- {configaroo-0.2.0 → configaroo-0.2.2}/tests/test_toml.py +0 -0
@@ -17,4 +17,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
17
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
18
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
19
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
-
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -7,4 +7,4 @@ Configaroo is a light configuration package for Python that offers the following
|
|
7
7
|
- Override key configuration settings with environment variables
|
8
8
|
- Validate a configuration based on a Pydantic model
|
9
9
|
- Convert the type of configuration values based on a Pydantic model
|
10
|
-
- Dynamically format certain configuration values
|
10
|
+
- Dynamically format certain configuration values
|
@@ -40,6 +40,7 @@ build = ["build>=1.2.2.post1", "twine>=6.1.0"]
|
|
40
40
|
dev = [
|
41
41
|
"bumpver>=2024.1130",
|
42
42
|
"ipython>=8.36.0",
|
43
|
+
"pre-commit>=4.2.0",
|
43
44
|
"pytest>=8.3.5",
|
44
45
|
"ruff>=0.11.11",
|
45
46
|
"tomli-w>=1.2.0",
|
@@ -50,13 +51,13 @@ dev = [
|
|
50
51
|
version = { attr = "configaroo.__version__" }
|
51
52
|
|
52
53
|
[tool.bumpver]
|
53
|
-
current_version = "v0.2.
|
54
|
+
current_version = "v0.2.2"
|
54
55
|
version_pattern = "vMAJOR.MINOR.PATCH"
|
55
56
|
commit_message = "bump version {old_version} -> {new_version}"
|
56
57
|
tag_message = "{new_version}"
|
57
58
|
commit = true
|
58
59
|
tag = true
|
59
|
-
push =
|
60
|
+
push = true
|
60
61
|
|
61
62
|
[tool.bumpver.file_patterns]
|
62
63
|
"pyproject.toml" = ['current_version = "{version}"']
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"""Bouncy configuration handling"""
|
2
|
+
|
3
|
+
from configaroo.configuration import Configuration
|
4
|
+
from configaroo.exceptions import (
|
5
|
+
ConfigarooException,
|
6
|
+
MissingEnvironmentVariableError,
|
7
|
+
UnsupportedLoaderError,
|
8
|
+
)
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"Configuration",
|
12
|
+
"ConfigarooException",
|
13
|
+
"MissingEnvironmentVariableError",
|
14
|
+
"UnsupportedLoaderError",
|
15
|
+
]
|
16
|
+
|
17
|
+
__version__ = "0.2.2"
|
@@ -18,6 +18,19 @@ ModelT = TypeVar("ModelT", bound=BaseModel)
|
|
18
18
|
class Configuration(UserDict):
|
19
19
|
"""A Configuration is a dict-like structure with some conveniences"""
|
20
20
|
|
21
|
+
@classmethod
|
22
|
+
def from_dict(cls, data: dict[str, Any] | UserDict[str, Any] | Self) -> Self:
|
23
|
+
"""Construct a Configuration from a dictionary
|
24
|
+
|
25
|
+
The dictionary is referenced directly, a copy isn't made
|
26
|
+
"""
|
27
|
+
configuration = cls()
|
28
|
+
if isinstance(data, UserDict | Configuration):
|
29
|
+
configuration.data = data.data
|
30
|
+
else:
|
31
|
+
configuration.data = data
|
32
|
+
return configuration
|
33
|
+
|
21
34
|
@classmethod
|
22
35
|
def from_file(
|
23
36
|
cls,
|
@@ -54,7 +67,7 @@ class Configuration(UserDict):
|
|
54
67
|
"""Make sure nested sections have type Configuration"""
|
55
68
|
value = self.data[key]
|
56
69
|
if isinstance(value, dict | UserDict | Configuration):
|
57
|
-
return Configuration(value)
|
70
|
+
return Configuration.from_dict(value)
|
58
71
|
else:
|
59
72
|
return value
|
60
73
|
|
@@ -67,11 +80,11 @@ class Configuration(UserDict):
|
|
67
80
|
f"'{type(self).__name__}' has no attribute or key '{key}'"
|
68
81
|
)
|
69
82
|
|
70
|
-
def __contains__(self, key:
|
83
|
+
def __contains__(self, key: object) -> bool:
|
71
84
|
"""Add support for dotted keys"""
|
72
85
|
if key in self.data:
|
73
86
|
return True
|
74
|
-
prefix, _, rest = key.partition(".")
|
87
|
+
prefix, _, rest = str(key).partition(".")
|
75
88
|
try:
|
76
89
|
return rest in self[prefix]
|
77
90
|
except KeyError:
|
@@ -108,18 +121,20 @@ class Configuration(UserDict):
|
|
108
121
|
)
|
109
122
|
return self
|
110
123
|
|
111
|
-
def parse_dynamic(
|
124
|
+
def parse_dynamic(
|
125
|
+
self, extra: dict[str, Any] | None = None, _include_self: bool = True
|
126
|
+
) -> Self:
|
112
127
|
"""Parse dynamic values of the form {section.key}"""
|
113
128
|
cls = type(self)
|
114
129
|
variables = (
|
115
|
-
self.to_flat_dict()
|
130
|
+
(self.to_flat_dict() if _include_self else {})
|
116
131
|
| {"project_path": _find_pyproject_toml()}
|
117
132
|
| ({} if extra is None else extra)
|
118
133
|
)
|
119
|
-
|
134
|
+
parsed = cls(
|
120
135
|
{
|
121
136
|
key: (
|
122
|
-
value.parse_dynamic(extra=variables)
|
137
|
+
value.parse_dynamic(extra=variables, _include_self=False)
|
123
138
|
if isinstance(value, Configuration)
|
124
139
|
else _incomplete_format(value, variables)
|
125
140
|
if isinstance(value, str)
|
@@ -128,6 +143,10 @@ class Configuration(UserDict):
|
|
128
143
|
for key, value in self.items()
|
129
144
|
}
|
130
145
|
)
|
146
|
+
if parsed == self:
|
147
|
+
return parsed
|
148
|
+
# Continue parsing until no more replacements are made.
|
149
|
+
return parsed.parse_dynamic(extra=extra, _include_self=_include_self)
|
131
150
|
|
132
151
|
def validate_model(self, model: Type[BaseModel]) -> Self:
|
133
152
|
"""Validate the configuration against the given model."""
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
4
4
|
|
5
5
|
import pytest
|
6
6
|
|
7
|
+
import configaroo
|
7
8
|
from configaroo import Configuration, configuration
|
8
9
|
|
9
10
|
|
@@ -71,6 +72,24 @@ def test_update_preserves_type(config):
|
|
71
72
|
assert isinstance(config, Configuration)
|
72
73
|
|
73
74
|
|
75
|
+
def test_update_changes_values(config):
|
76
|
+
"""Test that an update adds or changes values"""
|
77
|
+
updated_config = config | {"number": 14, "new": "brand new!"}
|
78
|
+
assert updated_config.number == 14
|
79
|
+
assert updated_config.new == "brand new!"
|
80
|
+
|
81
|
+
config.update({"number": 14, "new": "brand new!"})
|
82
|
+
assert config.number == 14
|
83
|
+
assert config.new == "brand new!"
|
84
|
+
|
85
|
+
|
86
|
+
def test_update_nested_values(config):
|
87
|
+
"""Test that a nested section can be updated"""
|
88
|
+
config.nested.deep.update({"sea": "Mjoesa", "depth": 456})
|
89
|
+
assert config.nested.deep.sea == "Mjoesa"
|
90
|
+
assert config.nested.deep.depth == 456
|
91
|
+
|
92
|
+
|
74
93
|
def test_dump_to_dict(config):
|
75
94
|
"""Test that dumping to a dictionary unwraps nested sections"""
|
76
95
|
config_dict = config.to_dict()
|
@@ -101,54 +120,6 @@ def test_contains_with_dotted_key(config):
|
|
101
120
|
assert "nested.number" not in config
|
102
121
|
|
103
122
|
|
104
|
-
def test_parse_dynamic_default(config, file_path):
|
105
|
-
"""Test parsing of default dynamic variables"""
|
106
|
-
parsed_config = (config | {"diameter": "2 x {nested.pie}"}).parse_dynamic()
|
107
|
-
print("pyproject.toml dir: ", configuration._find_pyproject_toml(file_path))
|
108
|
-
print(f"{parsed_config.paths.dynamic = }")
|
109
|
-
assert parsed_config.paths.dynamic == str(file_path)
|
110
|
-
assert parsed_config.phrase == "The meaning of life is 42"
|
111
|
-
assert parsed_config.diameter == "2 x 3.14"
|
112
|
-
|
113
|
-
|
114
|
-
def test_parse_dynamic_extra(config, file_path):
|
115
|
-
"""Test parsing of extra dynamic variables"""
|
116
|
-
parsed_config = (config | {"animal": "{adjective} kangaroo"}).parse_dynamic(
|
117
|
-
extra={"number": 14, "adjective": "bouncy"}
|
118
|
-
)
|
119
|
-
assert parsed_config.paths.dynamic == str(file_path)
|
120
|
-
assert parsed_config.phrase == "The meaning of life is 14"
|
121
|
-
assert parsed_config.animal == "bouncy kangaroo"
|
122
|
-
|
123
|
-
|
124
|
-
def test_parse_dynamic_formatted(config):
|
125
|
-
"""Test that formatting works for dynamic variables"""
|
126
|
-
parsed_config = (
|
127
|
-
config
|
128
|
-
| {
|
129
|
-
"string": "Hey {word!r}",
|
130
|
-
"three": "->{nested.pie:6.0f}<-",
|
131
|
-
"centered": "|{word:^12}|",
|
132
|
-
}
|
133
|
-
).parse_dynamic()
|
134
|
-
assert parsed_config.centered == "| platypus |"
|
135
|
-
assert parsed_config.three == "-> 3<-"
|
136
|
-
assert parsed_config.string == "Hey 'platypus'"
|
137
|
-
|
138
|
-
|
139
|
-
def test_parse_dynamic_ignore(config):
|
140
|
-
"""Test that parsing of dynamic variables ignores unknown replacements"""
|
141
|
-
parsed_config = (
|
142
|
-
config
|
143
|
-
| {
|
144
|
-
"animal": "{adjective} kangaroo",
|
145
|
-
"phrase": "one {nested.non_existent} dollar",
|
146
|
-
}
|
147
|
-
).parse_dynamic()
|
148
|
-
assert parsed_config.animal == "{adjective} kangaroo"
|
149
|
-
assert parsed_config.phrase == "one {nested.non_existent} dollar"
|
150
|
-
|
151
|
-
|
152
123
|
def test_find_pyproject_toml():
|
153
124
|
"""Test that the pyproject.toml file can be located"""
|
154
125
|
assert configuration._find_pyproject_toml() == Path(__file__).parent.parent
|
@@ -166,3 +137,9 @@ def test_incomplete_formatter():
|
|
166
137
|
{"number": 3.14, "string": "platypus", "name": "Geir Arne"},
|
167
138
|
)
|
168
139
|
assert formatted == " 3.1 {non_existent} 'platypus' Geir Arne"
|
140
|
+
|
141
|
+
|
142
|
+
def test_public_classes_are_exposed():
|
143
|
+
"""Test that the __all__ attribute exposes all public classes"""
|
144
|
+
public_classes = [attr for attr in dir(configaroo) if "A" <= attr[:1] <= "Z"]
|
145
|
+
assert sorted(public_classes) == sorted(configaroo.__all__)
|
@@ -0,0 +1,69 @@
|
|
1
|
+
"""Test handling of dynamic variables"""
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
import pytest
|
6
|
+
|
7
|
+
|
8
|
+
@pytest.fixture
|
9
|
+
def file_path():
|
10
|
+
"""The path to the current file"""
|
11
|
+
return Path(__file__).resolve()
|
12
|
+
|
13
|
+
|
14
|
+
def test_parse_dynamic_default(config, file_path):
|
15
|
+
"""Test parsing of default dynamic variables"""
|
16
|
+
parsed_config = (config | {"diameter": "2 x {nested.pie}"}).parse_dynamic()
|
17
|
+
assert parsed_config.paths.dynamic == str(file_path)
|
18
|
+
assert parsed_config.phrase == "The meaning of life is 42"
|
19
|
+
assert parsed_config.diameter == "2 x 3.14"
|
20
|
+
|
21
|
+
|
22
|
+
def test_parse_dynamic_extra(config, file_path):
|
23
|
+
"""Test parsing of extra dynamic variables"""
|
24
|
+
parsed_config = (config | {"animal": "{adjective} kangaroo"}).parse_dynamic(
|
25
|
+
extra={"number": 14, "adjective": "bouncy"}
|
26
|
+
)
|
27
|
+
assert parsed_config.paths.dynamic == str(file_path)
|
28
|
+
assert parsed_config.phrase == "The meaning of life is 14"
|
29
|
+
assert parsed_config.animal == "bouncy kangaroo"
|
30
|
+
|
31
|
+
|
32
|
+
def test_parse_dynamic_formatted(config):
|
33
|
+
"""Test that formatting works for dynamic variables"""
|
34
|
+
parsed_config = (
|
35
|
+
config
|
36
|
+
| {
|
37
|
+
"string": "Hey {word!r}",
|
38
|
+
"three": "->{nested.pie:6.0f}<-",
|
39
|
+
"centered": "|{word:^12}|",
|
40
|
+
}
|
41
|
+
).parse_dynamic()
|
42
|
+
assert parsed_config.centered == "| platypus |"
|
43
|
+
assert parsed_config.three == "-> 3<-"
|
44
|
+
assert parsed_config.string == "Hey 'platypus'"
|
45
|
+
|
46
|
+
|
47
|
+
def test_parse_dynamic_ignore(config):
|
48
|
+
"""Test that parsing of dynamic variables ignores unknown replacements"""
|
49
|
+
parsed_config = (
|
50
|
+
config
|
51
|
+
| {
|
52
|
+
"animal": "{adjective} kangaroo",
|
53
|
+
"phrase": "one {nested.non_existent} dollar",
|
54
|
+
}
|
55
|
+
).parse_dynamic()
|
56
|
+
assert parsed_config.animal == "{adjective} kangaroo"
|
57
|
+
assert parsed_config.phrase == "one {nested.non_existent} dollar"
|
58
|
+
|
59
|
+
|
60
|
+
def test_parse_dynamic_nested(config, file_path):
|
61
|
+
"""Test that parsing dynamic variables referring to other dynamic variables work"""
|
62
|
+
parsed_config = config.parse_dynamic()
|
63
|
+
assert parsed_config.paths.nested == str(file_path)
|
64
|
+
|
65
|
+
|
66
|
+
def test_parse_dynamic_only_full_name(config):
|
67
|
+
"""Test that parsing dynamic variables only use full dotted name"""
|
68
|
+
parsed_config = config.parse_dynamic()
|
69
|
+
assert parsed_config.log.format == config.log.format
|
@@ -53,3 +53,9 @@ def test_validate_and_convert(config, model):
|
|
53
53
|
config_w_model = config.with_model(model)
|
54
54
|
assert isinstance(config_w_model, pydantic.BaseModel)
|
55
55
|
assert isinstance(config_w_model.paths.relative, Path)
|
56
|
+
|
57
|
+
|
58
|
+
def test_convert_to_path(config, model):
|
59
|
+
paths_cfg = config.parse_dynamic().with_model(model).paths
|
60
|
+
assert isinstance(paths_cfg.relative, Path) and paths_cfg.relative.exists()
|
61
|
+
assert isinstance(paths_cfg.directory, Path) and paths_cfg.directory.is_dir()
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|