appsettings2 1.3.7__tar.gz → 2.0.6__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.
- {appsettings2-1.3.7/src/appsettings2.egg-info → appsettings2-2.0.6}/PKG-INFO +19 -20
- {appsettings2-1.3.7 → appsettings2-2.0.6}/README.md +13 -13
- {appsettings2-1.3.7 → appsettings2-2.0.6}/pyproject.toml +13 -7
- {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/Configuration.py +141 -84
- {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/ConfigurationBuilder.py +59 -27
- appsettings2-2.0.6/src/appsettings2/ConfigurationException.py +12 -0
- {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/__init__.py +8 -5
- appsettings2-2.0.6/src/appsettings2/helpers.py +44 -0
- {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/providers/CommandLineConfigurationProvider.py +8 -8
- appsettings2-2.0.6/src/appsettings2/providers/ConfigurationProvider.py +20 -0
- {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/providers/EnvironmentConfigurationProvider.py +3 -4
- {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/providers/JsonConfigurationProvider.py +29 -27
- {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/providers/TomlConfigurationProvider.py +27 -26
- {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/providers/YamlConfigurationProvider.py +30 -25
- {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/providers/__init__.py +1 -0
- {appsettings2-1.3.7 → appsettings2-2.0.6/src/appsettings2.egg-info}/PKG-INFO +19 -20
- {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2.egg-info/requires.txt +4 -1
- appsettings2-1.3.7/src/appsettings2/ConfigurationException.py +0 -10
- appsettings2-1.3.7/src/appsettings2/helpers.py +0 -29
- appsettings2-1.3.7/src/appsettings2/providers/ConfigurationProvider.py +0 -17
- {appsettings2-1.3.7 → appsettings2-2.0.6}/LICENSE +0 -0
- {appsettings2-1.3.7 → appsettings2-2.0.6}/setup.cfg +0 -0
- {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/py.typed +0 -0
- {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2.egg-info/SOURCES.txt +0 -0
- {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2.egg-info/dependency_links.txt +0 -0
- {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: appsettings2
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.6
|
|
4
4
|
Summary: Unifies configuration sources into a Configuration object that can be bound to complex types, or accessed directly for configuration data.
|
|
5
5
|
Author-email: Shaun Wilson <mrshaunwilson@msn.com>
|
|
6
6
|
License: MIT License
|
|
@@ -31,19 +31,18 @@ Project-URL: Repository, https://github.com/wilson0x4d/appsettings2.git
|
|
|
31
31
|
Keywords: appsettings,configuration
|
|
32
32
|
Classifier: Intended Audience :: Developers
|
|
33
33
|
Classifier: Operating System :: OS Independent
|
|
34
|
-
Classifier: Programming Language :: Python
|
|
35
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
36
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
37
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
38
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
34
|
+
Classifier: Programming Language :: Python
|
|
39
35
|
Requires-Python: >=3.11
|
|
40
36
|
Description-Content-Type: text/markdown
|
|
41
37
|
License-File: LICENSE
|
|
42
38
|
Requires-Dist: pyyaml>=6.0.2
|
|
43
39
|
Provides-Extra: dev
|
|
44
40
|
Requires-Dist: coverage; extra == "dev"
|
|
41
|
+
Requires-Dist: flake8; extra == "dev"
|
|
42
|
+
Requires-Dist: flake8-pyproject; extra == "dev"
|
|
45
43
|
Requires-Dist: mypy; extra == "dev"
|
|
46
|
-
Requires-Dist:
|
|
44
|
+
Requires-Dist: pep8-naming; extra == "dev"
|
|
45
|
+
Requires-Dist: punit>=1.4.9; extra == "dev"
|
|
47
46
|
Requires-Dist: twine; extra == "dev"
|
|
48
47
|
Requires-Dist: types-PyYAML; extra == "dev"
|
|
49
48
|
Dynamic: license-file
|
|
@@ -69,9 +68,9 @@ from appsettings2 import *
|
|
|
69
68
|
from appsettings2.providers import *
|
|
70
69
|
|
|
71
70
|
config = ConfigurationBuilder()\
|
|
72
|
-
.
|
|
73
|
-
.
|
|
74
|
-
.
|
|
71
|
+
.add_provider(JsonConfigurationProvider(f'appsettings.json'))\
|
|
72
|
+
.add_provider(JsonConfigurationProvider(f'appsettings.Development.json', required=False))\
|
|
73
|
+
.add_provider(EnvironmentConfigurationProvider())\
|
|
75
74
|
.build()
|
|
76
75
|
|
|
77
76
|
print(config)
|
|
@@ -132,15 +131,15 @@ json = """{
|
|
|
132
131
|
|
|
133
132
|
class ConnStrs:
|
|
134
133
|
"""An ugly class name to demonstrate the class name does not matter."""
|
|
135
|
-
SampleDB:str
|
|
134
|
+
SampleDB: str
|
|
136
135
|
|
|
137
136
|
class AppSettings:
|
|
138
|
-
ConnectionStrings:ConnStrs
|
|
139
|
-
EnableSwagger:bool
|
|
140
|
-
MaxBatchSize:int
|
|
137
|
+
ConnectionStrings: ConnStrs
|
|
138
|
+
EnableSwagger: bool
|
|
139
|
+
MaxBatchSize: int
|
|
141
140
|
|
|
142
141
|
configuration = ConfigurationBuilder()\
|
|
143
|
-
.
|
|
142
|
+
.add_provider(JsonConfigurationProvider(json=json))
|
|
144
143
|
.build()
|
|
145
144
|
|
|
146
145
|
settings = AppSettings()
|
|
@@ -170,7 +169,7 @@ You can transform a `Configuration` instance into a dictionary (as a copy.) If y
|
|
|
170
169
|
|
|
171
170
|
```python
|
|
172
171
|
configuration = builder.build()
|
|
173
|
-
d = configuration.
|
|
172
|
+
d = configuration.to_dict()
|
|
174
173
|
print(d)
|
|
175
174
|
```
|
|
176
175
|
|
|
@@ -184,7 +183,7 @@ print(d)
|
|
|
184
183
|
|
|
185
184
|
### Custom Provider Development
|
|
186
185
|
|
|
187
|
-
You can implement custom Configuration Providers by subclassing `ConfigurationProvider` and implementing `
|
|
186
|
+
You can implement custom Configuration Providers by subclassing `ConfigurationProvider` and implementing `populate_configuration(...)`.
|
|
188
187
|
|
|
189
188
|
For a peek at the simplicity of provider implementation, this is `ConfigurationProvider`:
|
|
190
189
|
|
|
@@ -192,7 +191,7 @@ For a peek at the simplicity of provider implementation, this is `ConfigurationP
|
|
|
192
191
|
class ConfigurationProvider(abstract):
|
|
193
192
|
|
|
194
193
|
@abstractmethod
|
|
195
|
-
def
|
|
194
|
+
def populate_configuration(self, configuration:Configuration) -> None:
|
|
196
195
|
"""The ConfigurationProvider will populate the provided Configuration instance."""
|
|
197
196
|
pass
|
|
198
197
|
```
|
|
@@ -206,10 +205,10 @@ There are several helpers available which simplify loading configurations. Consi
|
|
|
206
205
|
```python
|
|
207
206
|
import appsettings2
|
|
208
207
|
|
|
209
|
-
config = appsettings2.
|
|
208
|
+
config = appsettings2.get_configuration()
|
|
210
209
|
```
|
|
211
210
|
|
|
212
|
-
You can read more about `
|
|
211
|
+
You can read more about `get_configuration` in [the docs](https://appsettings2.readthedocs.io/en/latest/ref/helpers/get_configuration.html).
|
|
213
212
|
|
|
214
213
|
## Contact
|
|
215
214
|
|
|
@@ -19,9 +19,9 @@ from appsettings2 import *
|
|
|
19
19
|
from appsettings2.providers import *
|
|
20
20
|
|
|
21
21
|
config = ConfigurationBuilder()\
|
|
22
|
-
.
|
|
23
|
-
.
|
|
24
|
-
.
|
|
22
|
+
.add_provider(JsonConfigurationProvider(f'appsettings.json'))\
|
|
23
|
+
.add_provider(JsonConfigurationProvider(f'appsettings.Development.json', required=False))\
|
|
24
|
+
.add_provider(EnvironmentConfigurationProvider())\
|
|
25
25
|
.build()
|
|
26
26
|
|
|
27
27
|
print(config)
|
|
@@ -82,15 +82,15 @@ json = """{
|
|
|
82
82
|
|
|
83
83
|
class ConnStrs:
|
|
84
84
|
"""An ugly class name to demonstrate the class name does not matter."""
|
|
85
|
-
SampleDB:str
|
|
85
|
+
SampleDB: str
|
|
86
86
|
|
|
87
87
|
class AppSettings:
|
|
88
|
-
ConnectionStrings:ConnStrs
|
|
89
|
-
EnableSwagger:bool
|
|
90
|
-
MaxBatchSize:int
|
|
88
|
+
ConnectionStrings: ConnStrs
|
|
89
|
+
EnableSwagger: bool
|
|
90
|
+
MaxBatchSize: int
|
|
91
91
|
|
|
92
92
|
configuration = ConfigurationBuilder()\
|
|
93
|
-
.
|
|
93
|
+
.add_provider(JsonConfigurationProvider(json=json))
|
|
94
94
|
.build()
|
|
95
95
|
|
|
96
96
|
settings = AppSettings()
|
|
@@ -120,7 +120,7 @@ You can transform a `Configuration` instance into a dictionary (as a copy.) If y
|
|
|
120
120
|
|
|
121
121
|
```python
|
|
122
122
|
configuration = builder.build()
|
|
123
|
-
d = configuration.
|
|
123
|
+
d = configuration.to_dict()
|
|
124
124
|
print(d)
|
|
125
125
|
```
|
|
126
126
|
|
|
@@ -134,7 +134,7 @@ print(d)
|
|
|
134
134
|
|
|
135
135
|
### Custom Provider Development
|
|
136
136
|
|
|
137
|
-
You can implement custom Configuration Providers by subclassing `ConfigurationProvider` and implementing `
|
|
137
|
+
You can implement custom Configuration Providers by subclassing `ConfigurationProvider` and implementing `populate_configuration(...)`.
|
|
138
138
|
|
|
139
139
|
For a peek at the simplicity of provider implementation, this is `ConfigurationProvider`:
|
|
140
140
|
|
|
@@ -142,7 +142,7 @@ For a peek at the simplicity of provider implementation, this is `ConfigurationP
|
|
|
142
142
|
class ConfigurationProvider(abstract):
|
|
143
143
|
|
|
144
144
|
@abstractmethod
|
|
145
|
-
def
|
|
145
|
+
def populate_configuration(self, configuration:Configuration) -> None:
|
|
146
146
|
"""The ConfigurationProvider will populate the provided Configuration instance."""
|
|
147
147
|
pass
|
|
148
148
|
```
|
|
@@ -156,10 +156,10 @@ There are several helpers available which simplify loading configurations. Consi
|
|
|
156
156
|
```python
|
|
157
157
|
import appsettings2
|
|
158
158
|
|
|
159
|
-
config = appsettings2.
|
|
159
|
+
config = appsettings2.get_configuration()
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
-
You can read more about `
|
|
162
|
+
You can read more about `get_configuration` in [the docs](https://appsettings2.readthedocs.io/en/latest/ref/helpers/get_configuration.html).
|
|
163
163
|
|
|
164
164
|
## Contact
|
|
165
165
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "appsettings2"
|
|
3
|
-
version = "
|
|
3
|
+
version = "2.0.6"
|
|
4
4
|
description = "Unifies configuration sources into a Configuration object that can be bound to complex types, or accessed directly for configuration data."
|
|
5
5
|
keywords = ["appsettings", "configuration"]
|
|
6
6
|
authors = [
|
|
@@ -11,11 +11,7 @@ license = {file = "LICENSE"}
|
|
|
11
11
|
classifiers = [
|
|
12
12
|
"Intended Audience :: Developers",
|
|
13
13
|
"Operating System :: OS Independent",
|
|
14
|
-
"Programming Language :: Python
|
|
15
|
-
"Programming Language :: Python :: 3.12",
|
|
16
|
-
"Programming Language :: Python :: 3.13",
|
|
17
|
-
"Programming Language :: Python :: 3.14",
|
|
18
|
-
"Topic :: Software Development :: Libraries :: Python Modules"
|
|
14
|
+
"Programming Language :: Python"
|
|
19
15
|
]
|
|
20
16
|
requires-python = ">=3.11"
|
|
21
17
|
dependencies = [
|
|
@@ -25,8 +21,11 @@ dependencies = [
|
|
|
25
21
|
[project.optional-dependencies]
|
|
26
22
|
dev = [
|
|
27
23
|
"coverage",
|
|
24
|
+
"flake8",
|
|
25
|
+
"flake8-pyproject",
|
|
28
26
|
"mypy",
|
|
29
|
-
"
|
|
27
|
+
"pep8-naming",
|
|
28
|
+
"punit (>=1.4.9)",
|
|
30
29
|
"twine",
|
|
31
30
|
"types-PyYAML"
|
|
32
31
|
]
|
|
@@ -50,3 +49,10 @@ exclude = [
|
|
|
50
49
|
|
|
51
50
|
[tool.setuptools.package-data]
|
|
52
51
|
"appsettings2" = ["py.typed"]
|
|
52
|
+
|
|
53
|
+
[tool.flake8]
|
|
54
|
+
max-line-length = 120
|
|
55
|
+
ignore = ["D100","E501"]
|
|
56
|
+
per-file-ignore = [
|
|
57
|
+
"tests/*.py:D012",
|
|
58
|
+
]
|
|
@@ -1,53 +1,63 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: © 2025 Shaun Wilson
|
|
2
2
|
# SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
5
6
|
import json
|
|
7
|
+
import logging
|
|
6
8
|
import re
|
|
7
9
|
import types
|
|
8
|
-
import
|
|
9
|
-
from typing import Any, TYPE_CHECKING
|
|
10
|
+
from types import NoneType
|
|
11
|
+
from typing import Any, Iterator, Union, TYPE_CHECKING, get_args, get_origin, get_type_hints
|
|
10
12
|
import unicodedata
|
|
11
13
|
|
|
12
14
|
from .ConfigurationException import ConfigurationException
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class Configuration:
|
|
16
|
-
"""
|
|
17
|
-
The :py:class:`~appsettings2.Configuration` class is how applications access configuration data populated by :py:class:`~appsettings2.providers.ConfigurationProvider` objects. It exposes configuration data through dynamic object attributes as well as a dictionary-like interface.
|
|
18
|
-
"""
|
|
18
|
+
"""The :py:class:`~appsettings2.Configuration` class is how applications access configuration data populated by :py:class:`~appsettings2.providers.ConfigurationProvider` objects. It exposes configuration data through dynamic object attributes as well as a dictionary-like interface."""
|
|
19
19
|
|
|
20
|
-
__key_scrub_re:re.Pattern|None
|
|
21
|
-
__keys:dict[str, str]
|
|
22
|
-
__normalize:bool
|
|
20
|
+
__key_scrub_re: re.Pattern[str] | None
|
|
21
|
+
__keys: dict[str, str]
|
|
22
|
+
__normalize: bool
|
|
23
23
|
|
|
24
|
-
def __init__(self,
|
|
24
|
+
def __init__(self, normalize: bool = False, scrubkeys: bool = False) -> None:
|
|
25
25
|
"""
|
|
26
|
+
Initialize *Configuration* instance.
|
|
27
|
+
|
|
26
28
|
:param normalize: Option indicating whether or not attribute names should be normalized to upper-case on the resulting :py:class:`~appsettings2.Configuration` object, defaults to False.
|
|
27
29
|
:param scrubkeys: Option indicating whether or not attribute names should be scrubbed to be compatible with the Python lexer, defaults to False.
|
|
28
30
|
"""
|
|
29
31
|
self.__keys = {}
|
|
30
32
|
self.__logger = logging.getLogger('appsettings2')
|
|
31
33
|
self.__normalize = normalize
|
|
32
|
-
self.__key_scrub_re =
|
|
34
|
+
self.__key_scrub_re = (
|
|
35
|
+
None
|
|
36
|
+
if not scrubkeys
|
|
37
|
+
else re.compile(r'[^A-Za-z0-9_]', re.IGNORECASE | re.UNICODE)
|
|
38
|
+
)
|
|
33
39
|
|
|
34
40
|
if TYPE_CHECKING:
|
|
35
|
-
# This tells mypy that accessing any attribute returns 'Any'
|
|
36
|
-
def __getattr__(self, name: str) -> Any: ...
|
|
37
|
-
|
|
38
|
-
# This tells mypy that setting any attribute is allowed
|
|
39
|
-
def __setattr__(self, name: str, value: Any) -> None: ...
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
def __getattr__(self, name: str) -> Any:
|
|
43
|
+
"""Tell type checkers that accessing any attribute returns any type."""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
47
|
+
"""Tell type checkers that setting any attribute to any value is allowed."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
def __delitem__(self, key: str) -> None:
|
|
51
|
+
"""Delete the specified configuration key."""
|
|
42
52
|
key = key.upper()
|
|
43
53
|
k = self.__keys.get(key)
|
|
44
54
|
if k is not None:
|
|
45
55
|
delattr(self, k)
|
|
46
56
|
self.__keys.pop(key)
|
|
47
57
|
|
|
48
|
-
def __getitem__(self, key:str) -> Any:
|
|
58
|
+
def __getitem__(self, key: str) -> Any:
|
|
49
59
|
"""
|
|
50
|
-
|
|
60
|
+
Get the configuration data associated with the specified configuration key.
|
|
51
61
|
|
|
52
62
|
:param key: The configuration key to get data for. Supports `__` and `:` hierarchical delimiters.
|
|
53
63
|
:return: The configuration data associated with `key`, otherwise raises `KeyError` if `key` was not found.
|
|
@@ -65,27 +75,37 @@ class Configuration:
|
|
|
65
75
|
o = o[part]
|
|
66
76
|
return o
|
|
67
77
|
|
|
68
|
-
def __iter__(self):
|
|
78
|
+
def __iter__(self) -> Iterator[str]:
|
|
79
|
+
"""Iterate configuration keys."""
|
|
69
80
|
return iter(self.__keys.values())
|
|
70
81
|
|
|
71
82
|
def __len__(self) -> int:
|
|
83
|
+
"""Get the number of configuration keys."""
|
|
72
84
|
return len(self.__keys)
|
|
73
85
|
|
|
74
|
-
def
|
|
86
|
+
def __deunionize(self, t: type) -> type:
|
|
87
|
+
if get_origin(t) is Union:
|
|
88
|
+
t = [e for e in get_args(t) if e is not NoneType][0]
|
|
89
|
+
return t
|
|
90
|
+
|
|
91
|
+
def __recursive_bind(self, target: object, source: Configuration | dict[str, Any]) -> Any:
|
|
75
92
|
if target is None:
|
|
76
93
|
return None
|
|
77
94
|
if hasattr(target, '__class__'):
|
|
78
|
-
|
|
95
|
+
target_type_hints = get_type_hints(
|
|
96
|
+
getattr(target, '__class__')
|
|
97
|
+
)
|
|
79
98
|
else:
|
|
80
|
-
|
|
81
|
-
names = set(dir(target) |
|
|
99
|
+
target_type_hints = get_type_hints(target)
|
|
100
|
+
names = set(dir(target) | target_type_hints.keys())
|
|
82
101
|
for aname in names:
|
|
83
102
|
if aname.startswith('_'):
|
|
84
103
|
continue
|
|
85
|
-
lval = None if not hasattr(
|
|
86
|
-
|
|
104
|
+
lval = None if not hasattr(
|
|
105
|
+
target, aname) else getattr(target, aname)
|
|
106
|
+
if isinstance(lval, (types.FunctionType, types.MethodType)):
|
|
87
107
|
continue
|
|
88
|
-
ahint =
|
|
108
|
+
ahint = target_type_hints.get(aname)
|
|
89
109
|
rval = source.get(aname)
|
|
90
110
|
if ahint is None:
|
|
91
111
|
# attr has no type hints, attempt to treat as a property
|
|
@@ -95,7 +115,8 @@ class Configuration:
|
|
|
95
115
|
lval = getattr(target, aname)
|
|
96
116
|
except AttributeError:
|
|
97
117
|
lval = None
|
|
98
|
-
self.__logger.debug(
|
|
118
|
+
self.__logger.debug(
|
|
119
|
+
f'Failed to bind {aname}', exc_info=True)
|
|
99
120
|
if (not hasattr(prop, 'fset') or getattr(prop, 'fset') is None):
|
|
100
121
|
# NOTE: lval is not settable
|
|
101
122
|
if rval is None:
|
|
@@ -107,9 +128,10 @@ class Configuration:
|
|
|
107
128
|
continue
|
|
108
129
|
elif not (issubclass(type(lval), list) and issubclass(type(rval), list)):
|
|
109
130
|
# non-list values cannot be merged
|
|
110
|
-
raise Exception(
|
|
111
|
-
|
|
112
|
-
|
|
131
|
+
raise Exception(
|
|
132
|
+
f'Cannot bind `None` to attribute `{aname}`')
|
|
133
|
+
phints = get_type_hints(getattr(prop, 'fget'))
|
|
134
|
+
if phints is None:
|
|
113
135
|
# NOTE: can't get hints from getter, can't bind
|
|
114
136
|
continue
|
|
115
137
|
ahint = phints.get('return')
|
|
@@ -125,67 +147,83 @@ class Configuration:
|
|
|
125
147
|
elif ahint is str:
|
|
126
148
|
setattr(target, aname, str(rval))
|
|
127
149
|
elif isinstance(rval, Configuration):
|
|
128
|
-
if
|
|
129
|
-
lval = rval.
|
|
150
|
+
if get_origin(ahint) is dict:
|
|
151
|
+
lval = rval.to_dict()
|
|
130
152
|
setattr(target, aname, lval)
|
|
131
153
|
else:
|
|
132
154
|
if lval is None:
|
|
155
|
+
ahint = self.__deunionize(ahint)
|
|
133
156
|
lval = ahint()
|
|
134
157
|
setattr(target, aname, lval)
|
|
135
|
-
self.
|
|
136
|
-
elif
|
|
137
|
-
|
|
158
|
+
self.__recursive_bind(lval, rval)
|
|
159
|
+
elif get_origin(ahint) is list:
|
|
160
|
+
element_type = ahint.__args__[0]
|
|
138
161
|
if lval is None:
|
|
162
|
+
ahint = self.__deunionize(ahint)
|
|
139
163
|
lval = ahint()
|
|
140
164
|
setattr(target, aname, lval)
|
|
141
165
|
for e in rval:
|
|
142
166
|
lval.append(
|
|
143
|
-
self.
|
|
167
|
+
self.__recursive_bind_type(element_type, e)
|
|
168
|
+
)
|
|
169
|
+
elif get_origin(ahint) is set:
|
|
170
|
+
element_type = ahint.__args__[0]
|
|
171
|
+
if lval is None:
|
|
172
|
+
ahint = self.__deunionize(ahint)
|
|
173
|
+
lval = ahint()
|
|
174
|
+
setattr(target, aname, lval)
|
|
175
|
+
for e in rval:
|
|
176
|
+
lval.add(
|
|
177
|
+
self.__recursive_bind_type(element_type, e)
|
|
144
178
|
)
|
|
145
179
|
else:
|
|
146
180
|
setattr(target, aname, rval)
|
|
147
181
|
return target
|
|
148
182
|
|
|
149
|
-
def
|
|
150
|
-
if isinstance(source,
|
|
183
|
+
def __recursive_bind_type(self, element_type: type, source: Any) -> Any:
|
|
184
|
+
if isinstance(source, element_type):
|
|
151
185
|
return source
|
|
152
|
-
elif
|
|
186
|
+
elif element_type is float:
|
|
153
187
|
return float(source)
|
|
154
|
-
elif
|
|
188
|
+
elif element_type is int:
|
|
155
189
|
return int(source)
|
|
156
|
-
elif
|
|
190
|
+
elif element_type is str:
|
|
157
191
|
return str(source)
|
|
158
|
-
elif isinstance(source, Configuration
|
|
159
|
-
v =
|
|
160
|
-
return self.
|
|
192
|
+
elif isinstance(source, (Configuration, dict)):
|
|
193
|
+
v = element_type()
|
|
194
|
+
return self.__recursive_bind(v, source)
|
|
161
195
|
else:
|
|
162
|
-
raise ConfigurationException(
|
|
196
|
+
raise ConfigurationException(
|
|
197
|
+
f'Recursive bind to type `{element_type}` from `{type(source)}` is not supported.')
|
|
163
198
|
|
|
164
|
-
def __scrub_key(self, key:str) -> str:
|
|
199
|
+
def __scrub_key(self, key: str) -> str:
|
|
165
200
|
"""Scrubs a key for use as an attribute/identifier according to the Python lexer/standard."""
|
|
166
201
|
key = key.replace(':', '__').replace('.', '_')
|
|
167
202
|
return key if self.__key_scrub_re is None else self.__key_scrub_re.sub(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
203
|
+
self.__scrub_uc,
|
|
204
|
+
unicodedata.normalize(
|
|
205
|
+
'NFKC',
|
|
206
|
+
key))
|
|
172
207
|
|
|
173
|
-
def __scrub_uc(self, m:re.Match) -> str:
|
|
208
|
+
def __scrub_uc(self, m: re.Match[str]) -> str:
|
|
174
209
|
match unicodedata.category(m[0]):
|
|
175
|
-
case 'Lu' | 'Ll' | 'Lt' | 'Lm' | 'Lo' | 'Nl' | 'Mn' | 'Mc' | 'Nd' | 'Pc'
|
|
210
|
+
case 'Lu' | 'Ll' | 'Lt' | 'Lm' | 'Lo' | 'Nl' | 'Mn' | 'Mc' | 'Nd' | 'Pc':
|
|
176
211
|
return m[0]
|
|
177
212
|
case _:
|
|
178
213
|
return '_'
|
|
179
214
|
|
|
180
|
-
def __setitem__(self, key:str, value:Any) -> None:
|
|
215
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
216
|
+
"""Set configuration key to specified *value*."""
|
|
181
217
|
self.set(key, value)
|
|
182
218
|
|
|
183
219
|
def __str__(self) -> str:
|
|
184
|
-
|
|
220
|
+
"""Render the configuration as a JSON string."""
|
|
221
|
+
return json.dumps(self.to_dict(), indent=None, ensure_ascii=False)
|
|
185
222
|
|
|
186
|
-
def bind(self, target:object, key:str|None = None) -> Any:
|
|
223
|
+
def bind(self, target: object, key: str | None = None) -> Any:
|
|
187
224
|
"""
|
|
188
225
|
Binds the configuration values into the target object.
|
|
226
|
+
|
|
189
227
|
Can optionally specify a configuration key to bind from.
|
|
190
228
|
|
|
191
229
|
:param target: The object to bind configuration data into.
|
|
@@ -195,43 +233,50 @@ class Configuration:
|
|
|
195
233
|
if target is None:
|
|
196
234
|
raise ConfigurationException('Missing required argument: target')
|
|
197
235
|
if key is None:
|
|
198
|
-
return self.
|
|
236
|
+
return self.__recursive_bind(target, self)
|
|
199
237
|
else:
|
|
200
238
|
source = self.get(key)
|
|
201
239
|
if source is not None:
|
|
202
|
-
|
|
203
|
-
if
|
|
204
|
-
return self.
|
|
240
|
+
source_type = type(source)
|
|
241
|
+
if source_type is Configuration or source_type is dict:
|
|
242
|
+
return self.__recursive_bind(target, source)
|
|
205
243
|
else:
|
|
206
|
-
raise ConfigurationException(
|
|
244
|
+
raise ConfigurationException(
|
|
245
|
+
f'Bind of source type `{type(source)}` is not supported.')
|
|
207
246
|
return target
|
|
208
247
|
|
|
209
248
|
def clear(self) -> None:
|
|
249
|
+
"""Clear all configuration data."""
|
|
210
250
|
while len(self.__keys) > 0:
|
|
211
251
|
t = self.__keys.popitem()
|
|
212
252
|
delattr(self, t[1])
|
|
213
253
|
|
|
214
254
|
@staticmethod
|
|
215
|
-
def
|
|
255
|
+
def from_dict(source: dict[str, Any], normalize: bool = False, scrubkeys: bool = False) -> 'Configuration':
|
|
216
256
|
"""
|
|
217
|
-
|
|
257
|
+
Construct a :py:class:`~appsettings2.Configuration` instance from the supplied dictionary `source`.
|
|
218
258
|
|
|
219
259
|
:param source: The dictionary object to populate from.
|
|
220
260
|
:param normalize: Option indicating whether or not attribute names should be normalized to upper-case on the resulting :py:class:`~appsettings2.Configuration` object, defaults to False.
|
|
221
261
|
:param scrubkeys: Option indicating whether or not attribute names should be scrubbed to be compatible with the Python lexer, defaults to False.
|
|
222
262
|
:return: A :py:class:`~appsettings2.Configuration` object derived from the `source` parameter.
|
|
223
263
|
"""
|
|
224
|
-
config:Configuration = Configuration(
|
|
264
|
+
config: Configuration = Configuration(
|
|
265
|
+
normalize=normalize, scrubkeys=scrubkeys)
|
|
225
266
|
for kvp in source.items():
|
|
226
267
|
v = kvp[1]
|
|
227
268
|
if issubclass(type(v), dict):
|
|
228
|
-
v = Configuration.
|
|
269
|
+
v = Configuration.from_dict(
|
|
270
|
+
v, normalize=normalize, scrubkeys=scrubkeys)
|
|
229
271
|
config.set(kvp[0], v)
|
|
230
272
|
return config
|
|
231
273
|
|
|
232
|
-
|
|
274
|
+
fromDictionary = from_dict # noqa: N815
|
|
275
|
+
"""⚠️ DEPRECATED: use ``from_dict(...)`` instead."""
|
|
276
|
+
|
|
277
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
233
278
|
"""
|
|
234
|
-
|
|
279
|
+
Get the configuration data associated with the specified `key`.
|
|
235
280
|
|
|
236
281
|
:param key: The configuration key to get data for. Supports `__` and `:` hierarchical delimiters.
|
|
237
282
|
:param default: The value to be returned if `key` does not exist, defaults to None
|
|
@@ -250,10 +295,12 @@ class Configuration:
|
|
|
250
295
|
o = o.get(part, default)
|
|
251
296
|
return o
|
|
252
297
|
|
|
253
|
-
def has_key(self, key:str) -> bool:
|
|
298
|
+
def has_key(self, key: str) -> bool:
|
|
299
|
+
"""Check for a specific configuration key."""
|
|
254
300
|
return self.__keys.get(key.upper()) is not None
|
|
255
301
|
|
|
256
|
-
def items(self) -> list[tuple[str,Any]]:
|
|
302
|
+
def items(self) -> list[tuple[str, Any]]:
|
|
303
|
+
"""Get all key-value pairs as individal ``tuple`` items."""
|
|
257
304
|
it = []
|
|
258
305
|
for k in self.keys():
|
|
259
306
|
v = self.get(k)
|
|
@@ -261,19 +308,21 @@ class Configuration:
|
|
|
261
308
|
return it
|
|
262
309
|
|
|
263
310
|
def keys(self) -> list[str]:
|
|
311
|
+
"""Get a list of all configuration keys."""
|
|
264
312
|
return list(self.__keys.values())
|
|
265
313
|
|
|
266
|
-
def pop(self, key:str) -> Any:
|
|
314
|
+
def pop(self, key: str) -> Any:
|
|
315
|
+
"""Delete a specific configuration key."""
|
|
267
316
|
value = self[key]
|
|
268
317
|
del self[key]
|
|
269
318
|
return value
|
|
270
319
|
|
|
271
|
-
def set(self, key:str, value:Any) -> None:
|
|
320
|
+
def set(self, key: str, value: Any) -> None:
|
|
272
321
|
"""
|
|
273
|
-
|
|
322
|
+
Set the configuration value for a configuration key.
|
|
274
323
|
|
|
275
|
-
:param key: The key to associate the configuration
|
|
276
|
-
:param value: The
|
|
324
|
+
:param key: The *key* to associate the configuration *value*.
|
|
325
|
+
:param value: The *value* to be associated with the configuration *key*.
|
|
277
326
|
"""
|
|
278
327
|
if self.__normalize:
|
|
279
328
|
key = key.upper()
|
|
@@ -283,7 +332,8 @@ class Configuration:
|
|
|
283
332
|
if o == self:
|
|
284
333
|
k = self.__keys.get(parts[i].upper())
|
|
285
334
|
if k is None:
|
|
286
|
-
c = Configuration(normalize=self.__normalize, scrubkeys=(
|
|
335
|
+
c = Configuration(normalize=self.__normalize, scrubkeys=(
|
|
336
|
+
self.__key_scrub_re is None))
|
|
287
337
|
setattr(o, self.__scrub_key(parts[i]), c)
|
|
288
338
|
self.__keys[parts[i].upper()] = parts[i]
|
|
289
339
|
o = c
|
|
@@ -291,19 +341,22 @@ class Configuration:
|
|
|
291
341
|
o = getattr(self, self.__scrub_key(k))
|
|
292
342
|
else:
|
|
293
343
|
if not o.has_key(parts[i]):
|
|
294
|
-
c = Configuration(normalize=self.__normalize, scrubkeys=(
|
|
344
|
+
c = Configuration(normalize=self.__normalize, scrubkeys=(
|
|
345
|
+
self.__key_scrub_re is None))
|
|
295
346
|
o.set(parts[i], c)
|
|
296
347
|
o = c
|
|
297
348
|
else:
|
|
298
349
|
o = o.get(parts[i])
|
|
299
350
|
vtype = type(value)
|
|
300
351
|
if issubclass(vtype, dict):
|
|
301
|
-
value = Configuration.
|
|
352
|
+
value = Configuration.from_dict(
|
|
353
|
+
value, normalize=self.__normalize, scrubkeys=self.__key_scrub_re is not None)
|
|
302
354
|
elif issubclass(vtype, list):
|
|
303
|
-
l = []
|
|
355
|
+
l: list[Any] = list[Any]()
|
|
304
356
|
for e in value:
|
|
305
357
|
if issubclass(type(e), dict):
|
|
306
|
-
l.append(Configuration.
|
|
358
|
+
l.append(Configuration.from_dict(
|
|
359
|
+
e, normalize=self.__normalize, scrubkeys=self.__key_scrub_re is not None))
|
|
307
360
|
else:
|
|
308
361
|
l.append(e)
|
|
309
362
|
value = l
|
|
@@ -318,22 +371,22 @@ class Configuration:
|
|
|
318
371
|
else:
|
|
319
372
|
o.set(key, value)
|
|
320
373
|
|
|
321
|
-
def
|
|
374
|
+
def to_dict(self) -> dict[str, Any]:
|
|
322
375
|
"""
|
|
323
|
-
|
|
376
|
+
Create a dictionary from the `Configuration` object.
|
|
324
377
|
|
|
325
378
|
:return: A dictionary containing all keys and their associated values, in a structure that mimics the structure if the data contained within the `Configuration` object.
|
|
326
379
|
"""
|
|
327
|
-
result = dict[str,Any]()
|
|
380
|
+
result = dict[str, Any]()
|
|
328
381
|
for k in self.__keys.values():
|
|
329
382
|
v = getattr(self, self.__scrub_key(k))
|
|
330
383
|
if isinstance(v, Configuration):
|
|
331
|
-
result[k] = v.
|
|
384
|
+
result[k] = v.to_dict()
|
|
332
385
|
elif issubclass(type(v), list):
|
|
333
386
|
tmp = []
|
|
334
387
|
for e in v:
|
|
335
388
|
if isinstance(e, Configuration):
|
|
336
|
-
tmp.append(e.
|
|
389
|
+
tmp.append(e.to_dict())
|
|
337
390
|
else:
|
|
338
391
|
tmp.append(e)
|
|
339
392
|
result[k] = tmp
|
|
@@ -341,7 +394,11 @@ class Configuration:
|
|
|
341
394
|
result[k] = v
|
|
342
395
|
return result
|
|
343
396
|
|
|
397
|
+
toDictionary = to_dict # noqa: N815
|
|
398
|
+
"""⚠️ DEPRECATED: use ``to_dict(...)`` instead."""
|
|
399
|
+
|
|
344
400
|
def values(self) -> list[Any]:
|
|
401
|
+
"""Get a list of all configuration values."""
|
|
345
402
|
values = []
|
|
346
403
|
for k in self.__keys.values():
|
|
347
404
|
v = getattr(self, self.__scrub_key(k))
|