configaroo 0.2.4__tar.gz → 0.4.0__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.4/src/configaroo.egg-info → configaroo-0.4.0}/PKG-INFO +1 -1
- {configaroo-0.2.4 → configaroo-0.4.0}/pyproject.toml +5 -6
- {configaroo-0.2.4 → configaroo-0.4.0}/src/configaroo/__init__.py +1 -1
- {configaroo-0.2.4 → configaroo-0.4.0}/src/configaroo/configuration.py +35 -32
- {configaroo-0.2.4 → configaroo-0.4.0}/src/configaroo/loaders/__init__.py +6 -1
- {configaroo-0.2.4 → configaroo-0.4.0/src/configaroo.egg-info}/PKG-INFO +1 -1
- {configaroo-0.2.4 → configaroo-0.4.0}/tests/test_configuration.py +7 -7
- {configaroo-0.2.4 → configaroo-0.4.0}/tests/test_environment.py +15 -1
- {configaroo-0.2.4 → configaroo-0.4.0}/tests/test_json.py +6 -0
- {configaroo-0.2.4 → configaroo-0.4.0}/tests/test_toml.py +6 -0
- {configaroo-0.2.4 → configaroo-0.4.0}/LICENSE +0 -0
- {configaroo-0.2.4 → configaroo-0.4.0}/README.md +0 -0
- {configaroo-0.2.4 → configaroo-0.4.0}/setup.cfg +0 -0
- {configaroo-0.2.4 → configaroo-0.4.0}/src/configaroo/exceptions.py +0 -0
- {configaroo-0.2.4 → configaroo-0.4.0}/src/configaroo/loaders/json.py +0 -0
- {configaroo-0.2.4 → configaroo-0.4.0}/src/configaroo/loaders/toml.py +0 -0
- {configaroo-0.2.4 → configaroo-0.4.0}/src/configaroo/py.typed +0 -0
- {configaroo-0.2.4 → configaroo-0.4.0}/src/configaroo.egg-info/SOURCES.txt +0 -0
- {configaroo-0.2.4 → configaroo-0.4.0}/src/configaroo.egg-info/dependency_links.txt +0 -0
- {configaroo-0.2.4 → configaroo-0.4.0}/src/configaroo.egg-info/requires.txt +0 -0
- {configaroo-0.2.4 → configaroo-0.4.0}/src/configaroo.egg-info/top_level.txt +0 -0
- {configaroo-0.2.4 → configaroo-0.4.0}/tests/test_dynamic.py +0 -0
- {configaroo-0.2.4 → configaroo-0.4.0}/tests/test_loaders.py +0 -0
- {configaroo-0.2.4 → configaroo-0.4.0}/tests/test_print.py +0 -0
- {configaroo-0.2.4 → configaroo-0.4.0}/tests/test_validation.py +0 -0
@@ -36,16 +36,15 @@ changelog = "https://github.com/gahjelle/configaroo/releases"
|
|
36
36
|
|
37
37
|
[dependency-groups]
|
38
38
|
build = ["build>=1.2.2.post1", "twine>=6.1.0"]
|
39
|
+
ci = ["mypy>=1.17.1", "pytest>=8.4.1", "rich>=14.1.0", "ruff>=0.12.7", "tomli-w>=1.2.0"]
|
39
40
|
dev = [
|
41
|
+
{ include-group = "ci" },
|
42
|
+
{ include-group = "test" },
|
40
43
|
"bumpver>=2024.1130",
|
41
44
|
"ipython>=8.36.0",
|
42
|
-
"mypy>=1.17.1",
|
43
45
|
"pre-commit>=4.2.0",
|
44
|
-
"pytest>=8.3.5",
|
45
|
-
"rich>=14.1.0",
|
46
|
-
"ruff>=0.11.11",
|
47
|
-
"tomli-w>=1.2.0",
|
48
46
|
]
|
47
|
+
test = ["pytest>=8.4.1", "pytest-cov>=6.2.1", "tomli-w>=1.2.0"]
|
49
48
|
|
50
49
|
|
51
50
|
[tool.setuptools.dynamic]
|
@@ -80,7 +79,7 @@ python_version = "3.11"
|
|
80
79
|
strict = true
|
81
80
|
|
82
81
|
[tool.bumpver]
|
83
|
-
current_version = "v0.
|
82
|
+
current_version = "v0.4.0"
|
84
83
|
version_pattern = "vMAJOR.MINOR.PATCH"
|
85
84
|
commit_message = "bump version {old_version} -> {new_version}"
|
86
85
|
tag_message = "{new_version}"
|
@@ -36,33 +36,20 @@ class Configuration(UserDict[str, Any]):
|
|
36
36
|
def from_file(
|
37
37
|
cls,
|
38
38
|
file_path: str | Path,
|
39
|
+
*,
|
39
40
|
loader: str | None = None,
|
40
|
-
|
41
|
-
env_prefix: str = "",
|
42
|
-
extra_dynamic: dict[str, Any] | None = None,
|
41
|
+
not_exist_ok: bool = False,
|
43
42
|
) -> Self:
|
44
|
-
"""Read a Configuration from a file.
|
45
|
-
config_dict = loaders.from_file(file_path, loader=loader)
|
46
|
-
return cls(config_dict).initialize(
|
47
|
-
envs=envs, env_prefix=env_prefix, extra_dynamic=extra_dynamic
|
48
|
-
)
|
49
|
-
|
50
|
-
def initialize(
|
51
|
-
self,
|
52
|
-
envs: dict[str, str] | None = None,
|
53
|
-
env_prefix: str = "",
|
54
|
-
extra_dynamic: dict[str, Any] | None = None,
|
55
|
-
) -> Self:
|
56
|
-
"""Initialize a configuration.
|
43
|
+
"""Read a Configuration from a file.
|
57
44
|
|
58
|
-
|
45
|
+
If not_exist_ok is True, then a missing file returns an empty
|
46
|
+
configuration. This may be useful if the configuration is potentially
|
47
|
+
populated by environment variables.
|
59
48
|
"""
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
"""Apply a pydantic model to a configuration."""
|
65
|
-
return self.validate_model(model).convert_model(model)
|
49
|
+
config_dict = loaders.from_file(
|
50
|
+
file_path, loader=loader, not_exist_ok=not_exist_ok
|
51
|
+
)
|
52
|
+
return cls(config_dict)
|
66
53
|
|
67
54
|
def __getitem__(self, key: str) -> Any: # noqa: ANN401
|
68
55
|
"""Make sure nested sections have type Configuration."""
|
@@ -112,8 +99,22 @@ class Configuration(UserDict[str, Any]):
|
|
112
99
|
cls = type(self)
|
113
100
|
return self | {prefix: cls(self.setdefault(prefix, {})).add(rest, value)}
|
114
101
|
|
115
|
-
def add_envs(self, envs: dict[str, str], prefix: str = "") -> Self:
|
116
|
-
"""Add environment variables to configuration.
|
102
|
+
def add_envs(self, envs: dict[str, str] | None = None, prefix: str = "") -> Self:
|
103
|
+
"""Add environment variables to configuration.
|
104
|
+
|
105
|
+
If you don't specify which environment variables to read, you'll
|
106
|
+
automatically add any that matches a top-level value of the
|
107
|
+
configuration.
|
108
|
+
"""
|
109
|
+
if envs is None:
|
110
|
+
# Automatically add top-level configuration items
|
111
|
+
envs = {
|
112
|
+
re.sub(r"\W", "_", key).upper(): key
|
113
|
+
for key, value in self.data.items()
|
114
|
+
if isinstance(value, str | int | float)
|
115
|
+
}
|
116
|
+
|
117
|
+
# Read environment variables
|
117
118
|
for env, key in envs.items():
|
118
119
|
env_key = f"{prefix}{env}"
|
119
120
|
if env_value := os.getenv(env_key):
|
@@ -129,7 +130,7 @@ class Configuration(UserDict[str, Any]):
|
|
129
130
|
cls = type(self)
|
130
131
|
variables = (
|
131
132
|
(self.to_flat_dict() if _include_self else {})
|
132
|
-
| {"project_path":
|
133
|
+
| {"project_path": find_pyproject_toml()}
|
133
134
|
| ({} if extra is None else extra)
|
134
135
|
)
|
135
136
|
parsed = cls(
|
@@ -158,6 +159,10 @@ class Configuration(UserDict[str, Any]):
|
|
158
159
|
"""Convert data types to match the given model."""
|
159
160
|
return model(**self.data)
|
160
161
|
|
162
|
+
def with_model(self, model: type[ModelT]) -> ModelT:
|
163
|
+
"""Apply a pydantic model to a configuration."""
|
164
|
+
return self.validate_model(model).convert_model(model)
|
165
|
+
|
161
166
|
def to_dict(self) -> dict[str, Any]:
|
162
167
|
"""Dump the configuration into a Python dictionary."""
|
163
168
|
return {
|
@@ -196,16 +201,14 @@ def print_configuration(config: Configuration | BaseModel, indent: int = 4) -> N
|
|
196
201
|
)
|
197
202
|
|
198
203
|
|
199
|
-
def _get_rich_print() -> Callable[[str], None]:
|
204
|
+
def _get_rich_print() -> Callable[[str], None]: # pragma: no cover
|
200
205
|
"""Initialize a Rich console if Rich is installed, otherwise use built-in print."""
|
201
206
|
try:
|
202
207
|
from rich.console import Console # noqa: PLC0415
|
203
208
|
|
204
209
|
return Console().print
|
205
210
|
except ImportError:
|
206
|
-
|
207
|
-
|
208
|
-
return builtins.print
|
211
|
+
return print
|
209
212
|
|
210
213
|
|
211
214
|
def _print_dict_as_tree(
|
@@ -228,7 +231,7 @@ def _print_dict_as_tree(
|
|
228
231
|
_print(" " * current_indent + f"- {key}: {value!r}")
|
229
232
|
|
230
233
|
|
231
|
-
def
|
234
|
+
def find_pyproject_toml(
|
232
235
|
path: Path | None = None, _file_name: str = "pyproject.toml"
|
233
236
|
) -> Path:
|
234
237
|
"""Find a directory that contains a pyproject.toml file.
|
@@ -241,7 +244,7 @@ def _find_pyproject_toml(
|
|
241
244
|
if (path / _file_name).exists() or path == path.parent:
|
242
245
|
return path.resolve()
|
243
246
|
|
244
|
-
return
|
247
|
+
return find_pyproject_toml(path.parent, _file_name=_file_name)
|
245
248
|
|
246
249
|
|
247
250
|
def _get_foreign_path() -> Path:
|
@@ -26,9 +26,14 @@ def loader_names() -> list[str]:
|
|
26
26
|
return sorted(pyplugs.names(PACKAGE))
|
27
27
|
|
28
28
|
|
29
|
-
def from_file(
|
29
|
+
def from_file(
|
30
|
+
path: str | Path, *, loader: str | None = None, not_exist_ok: bool = False
|
31
|
+
) -> dict[str, Any]:
|
30
32
|
"""Load a file using a loader defined by the suffix if necessary."""
|
31
33
|
path = Path(path)
|
34
|
+
if not path.exists() and not_exist_ok:
|
35
|
+
return {}
|
36
|
+
|
32
37
|
loader = path.suffix.lstrip(".") if loader is None else loader
|
33
38
|
try:
|
34
39
|
return load(loader, path=path)
|
@@ -8,12 +8,6 @@ import configaroo
|
|
8
8
|
from configaroo import Configuration, configuration
|
9
9
|
|
10
10
|
|
11
|
-
@pytest.fixture
|
12
|
-
def file_path() -> Path:
|
13
|
-
"""Return the path to the current file."""
|
14
|
-
return Path(__file__).resolve()
|
15
|
-
|
16
|
-
|
17
11
|
def test_read_simple_values_as_attributes(config: Configuration) -> None:
|
18
12
|
"""Test attribute access for simple values."""
|
19
13
|
assert config.number == 42
|
@@ -64,6 +58,12 @@ def test_get_nested_values(config: Configuration) -> None:
|
|
64
58
|
assert config.get("with_dot.org.num") == 1234
|
65
59
|
|
66
60
|
|
61
|
+
def test_get_with_default(config: Configuration) -> None:
|
62
|
+
"""Test that .get() falls back on default if the key doesn't exist."""
|
63
|
+
assert config.get("word", default="kangaroo") == "platypus"
|
64
|
+
assert config.get("another word", default="kangaroo") == "kangaroo"
|
65
|
+
|
66
|
+
|
67
67
|
def test_update_preserves_type(config: Configuration) -> None:
|
68
68
|
"""Test that an update operation gives a Configuration."""
|
69
69
|
assert isinstance(config | {"new": 1}, Configuration)
|
@@ -122,7 +122,7 @@ def test_contains_with_dotted_key(config: Configuration) -> None:
|
|
122
122
|
|
123
123
|
def test_find_pyproject_toml() -> None:
|
124
124
|
"""Test that the pyproject.toml file can be located."""
|
125
|
-
assert configuration.
|
125
|
+
assert configuration.find_pyproject_toml() == Path(__file__).parent.parent
|
126
126
|
|
127
127
|
|
128
128
|
def test_find_foreign_caller() -> None:
|
@@ -45,7 +45,7 @@ def test_missing_env_ok_if_optional(config: Configuration) -> None:
|
|
45
45
|
|
46
46
|
|
47
47
|
def test_env_prefix(config: Configuration, monkeypatch: pytest.MonkeyPatch) -> None:
|
48
|
-
"""Test that a common prefix can be used for
|
48
|
+
"""Test that a common prefix can be used for environment variables."""
|
49
49
|
monkeypatch.setenv("EXAMPLE_NUMBER", "14")
|
50
50
|
monkeypatch.setenv("EXAMPLE_WORD", "platypus")
|
51
51
|
config_w_env = config.add_envs(
|
@@ -53,3 +53,17 @@ def test_env_prefix(config: Configuration, monkeypatch: pytest.MonkeyPatch) -> N
|
|
53
53
|
)
|
54
54
|
assert config_w_env.number == "14"
|
55
55
|
assert config_w_env.nested.word == "platypus"
|
56
|
+
|
57
|
+
|
58
|
+
def test_env_automatic(config: Configuration, monkeypatch: pytest.MonkeyPatch) -> None:
|
59
|
+
"""Test that top-level keys can be automatically filled by env variables."""
|
60
|
+
monkeypatch.setenv("NUMBER", "28")
|
61
|
+
monkeypatch.setenv("WORD", "kangaroo")
|
62
|
+
monkeypatch.setenv("A_B_D_KEY_", "works")
|
63
|
+
monkeypatch.setenv("NESTED", "should not be replaced")
|
64
|
+
config_w_env = (config | {"A b@d-key!": ""}).add_envs()
|
65
|
+
|
66
|
+
assert config_w_env.number == "28"
|
67
|
+
assert config_w_env.word == "kangaroo"
|
68
|
+
assert config_w_env["A b@d-key!"] == "works"
|
69
|
+
assert config_w_env.nested != "should not be replaced"
|
@@ -38,6 +38,12 @@ def test_error_on_wrong_format(toml_path: Path) -> None:
|
|
38
38
|
Configuration.from_file(toml_path, loader="json")
|
39
39
|
|
40
40
|
|
41
|
+
def test_file_may_be_allowed_to_not_exist() -> None:
|
42
|
+
"""Test that not_exist_ok can suppress error when file doesn't exist."""
|
43
|
+
config = Configuration.from_file("non-existent.json", not_exist_ok=True)
|
44
|
+
assert config.data == {}
|
45
|
+
|
46
|
+
|
41
47
|
def test_can_read_json_values(json_path: Path) -> None:
|
42
48
|
"""Test that values can be accessed."""
|
43
49
|
config = Configuration.from_file(json_path)
|
@@ -38,6 +38,12 @@ def test_error_on_wrong_format(json_path: Path) -> None:
|
|
38
38
|
Configuration.from_file(json_path, loader="toml")
|
39
39
|
|
40
40
|
|
41
|
+
def test_file_may_be_allowed_to_not_exist() -> None:
|
42
|
+
"""Test that not_exist_ok can suppress error when file doesn't exist."""
|
43
|
+
config = Configuration.from_file("non-existent.toml", not_exist_ok=True)
|
44
|
+
assert config.data == {}
|
45
|
+
|
46
|
+
|
41
47
|
def test_can_read_toml_values(toml_path: Path) -> None:
|
42
48
|
"""Test that values can be accessed."""
|
43
49
|
config = Configuration.from_file(toml_path)
|
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
|
File without changes
|
File without changes
|