appsettings2 2.0.7__tar.gz → 2.0.9__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-2.0.7/src/appsettings2.egg-info → appsettings2-2.0.9}/PKG-INFO +1 -1
- {appsettings2-2.0.7 → appsettings2-2.0.9}/pyproject.toml +1 -1
- {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/Configuration.py +133 -3
- {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/__init__.py +2 -2
- {appsettings2-2.0.7 → appsettings2-2.0.9/src/appsettings2.egg-info}/PKG-INFO +1 -1
- {appsettings2-2.0.7 → appsettings2-2.0.9}/LICENSE +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/README.md +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/setup.cfg +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/ConfigurationBuilder.py +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/ConfigurationException.py +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/helpers.py +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/CommandLineConfigurationProvider.py +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/ConfigurationProvider.py +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/EnvironmentConfigurationProvider.py +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/JsonConfigurationProvider.py +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/TomlConfigurationProvider.py +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/YamlConfigurationProvider.py +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/__init__.py +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/py.typed +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2.egg-info/SOURCES.txt +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2.egg-info/dependency_links.txt +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2.egg-info/requires.txt +0 -0
- {appsettings2-2.0.7 → appsettings2-2.0.9}/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: 2.0.
|
|
3
|
+
Version: 2.0.9
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "appsettings2"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.9"
|
|
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 = [
|
|
@@ -86,6 +86,8 @@ class Configuration:
|
|
|
86
86
|
def __deunionize(self, t: type) -> type:
|
|
87
87
|
if get_origin(t) is Union:
|
|
88
88
|
t = [e for e in get_args(t) if e is not NoneType][0]
|
|
89
|
+
elif get_origin(t) is types.UnionType:
|
|
90
|
+
t = [e for e in get_args(t) if e is not NoneType][0]
|
|
89
91
|
return t
|
|
90
92
|
|
|
91
93
|
def __recursive_bind(self, target: object, source: Configuration | dict[str, Any]) -> Any:
|
|
@@ -97,6 +99,14 @@ class Configuration:
|
|
|
97
99
|
)
|
|
98
100
|
else:
|
|
99
101
|
target_type_hints = get_type_hints(target)
|
|
102
|
+
|
|
103
|
+
# build scrubbed/normalized key lookup (for matching)
|
|
104
|
+
norm_source: bool | None = None
|
|
105
|
+
key_scrub_re_cfg: re.Pattern[str] | None = None
|
|
106
|
+
if isinstance(source, Configuration):
|
|
107
|
+
norm_source = source._Configuration__normalize
|
|
108
|
+
key_scrub_re_cfg = source._Configuration__key_scrub_re
|
|
109
|
+
|
|
100
110
|
names = set(dir(target) | target_type_hints.keys())
|
|
101
111
|
for aname in names:
|
|
102
112
|
if aname.startswith('_'):
|
|
@@ -107,6 +117,20 @@ class Configuration:
|
|
|
107
117
|
continue
|
|
108
118
|
ahint = target_type_hints.get(aname)
|
|
109
119
|
rval = source.get(aname)
|
|
120
|
+
# scrub/normalize-aware; look up using transformed keys
|
|
121
|
+
if rval is None and norm_source is not None:
|
|
122
|
+
raw_src_key = aname.replace(':', '__')
|
|
123
|
+
for cfg_key in source.keys(): # type: ignore[union-attr]
|
|
124
|
+
raw = cfg_key.replace('.', '_')
|
|
125
|
+
candidate = raw.upper() if norm_source else raw
|
|
126
|
+
scrubbed = candidate if key_scrub_re_cfg is None \
|
|
127
|
+
else key_scrub_re_cfg.sub(
|
|
128
|
+
lambda m: '_' if unicodedata.category(m[0]) not in ('Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl', 'Mn', 'Mc', 'Nd', 'Pc') else m[0],
|
|
129
|
+
candidate)
|
|
130
|
+
compare = scrubbed.replace('-', '_')
|
|
131
|
+
if compare == raw_src_key or compare.upper() == raw_src_key.upper():
|
|
132
|
+
rval = source.get(cfg_key) # type: ignore[union-attr]
|
|
133
|
+
break
|
|
110
134
|
if ahint is None:
|
|
111
135
|
# attr has no type hints, attempt to treat as a property
|
|
112
136
|
prop = getattr(type(target), aname)
|
|
@@ -156,7 +180,37 @@ class Configuration:
|
|
|
156
180
|
lval[dk] = self.__recursive_bind_type(value_type, dv)
|
|
157
181
|
setattr(target, aname, lval)
|
|
158
182
|
continue
|
|
183
|
+
# Union/Optional types when rval missing but there's source data?
|
|
184
|
+
if get_origin(ahint) is Union or get_origin(ahint) is types.UnionType \
|
|
185
|
+
or (isinstance(ahint, type) and hasattr(ahint, '__origin__')):
|
|
186
|
+
unwrapped = self.__deunionize(ahint)
|
|
187
|
+
if unwrapped not in (str, int, float, bool, NoneType):
|
|
188
|
+
# source key overlaps with attr names?
|
|
189
|
+
src_key_overlap = False
|
|
190
|
+
for cfg_key in source.keys(): # type: ignore[union-attr]
|
|
191
|
+
raw_src_key = cfg_key.replace('.', '_')
|
|
192
|
+
candidate = raw_src_key.upper() if norm_source else raw_src_key
|
|
193
|
+
scrubbed = candidate if key_scrub_re_cfg is None \
|
|
194
|
+
else key_scrub_re_cfg.sub(
|
|
195
|
+
lambda m: '_' if unicodedata.category(m[0]) not in ('Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl', 'Mn', 'Mc', 'Nd', 'Pc') else m[0],
|
|
196
|
+
candidate)
|
|
197
|
+
compare = scrubbed.replace('-', '_').replace('_', '')
|
|
198
|
+
for other_attr in names:
|
|
199
|
+
if not other_attr.startswith('_'):
|
|
200
|
+
okey = other_attr.replace(':', '__')
|
|
201
|
+
if compare == okey or compare.upper() == okey.upper():
|
|
202
|
+
src_key_overlap = True
|
|
203
|
+
break
|
|
204
|
+
if src_key_overlap:
|
|
205
|
+
break
|
|
206
|
+
if isinstance(source, Configuration) and source.keys() and not src_key_overlap: # type: ignore[union-attr]
|
|
207
|
+
lval = unwrapped()
|
|
208
|
+
setattr(target, aname, lval)
|
|
209
|
+
self.__recursive_bind(lval, source)
|
|
210
|
+
continue
|
|
159
211
|
setattr(target, aname, None)
|
|
212
|
+
elif ahint is bool:
|
|
213
|
+
setattr(target, aname, bool(rval))
|
|
160
214
|
elif ahint is float:
|
|
161
215
|
setattr(target, aname, float(rval))
|
|
162
216
|
elif ahint is int:
|
|
@@ -174,6 +228,28 @@ class Configuration:
|
|
|
174
228
|
for dk, dv in rval.items():
|
|
175
229
|
lval[dk] = self.__recursive_bind_type(value_type, dv)
|
|
176
230
|
setattr(target, aname, lval)
|
|
231
|
+
elif (
|
|
232
|
+
get_origin(ahint) is Union
|
|
233
|
+
or get_origin(ahint) is types.UnionType
|
|
234
|
+
or (isinstance(ahint, type) and hasattr(ahint, '__origin__'))
|
|
235
|
+
):
|
|
236
|
+
# Handle Optional/Union types — unwrap before instantiating
|
|
237
|
+
unwrapped = self.__deunionize(ahint)
|
|
238
|
+
if get_origin(unwrapped) is dict:
|
|
239
|
+
value_type = get_args(unwrapped)[1]
|
|
240
|
+
if value_type in (str, int, float, bool, type(None)):
|
|
241
|
+
lval = rval.to_dict()
|
|
242
|
+
setattr(target, aname, lval)
|
|
243
|
+
else:
|
|
244
|
+
lval = dict[str, Any]()
|
|
245
|
+
for dk, dv in rval.items():
|
|
246
|
+
lval[dk] = self.__recursive_bind_type(value_type, dv)
|
|
247
|
+
setattr(target, aname, lval)
|
|
248
|
+
else:
|
|
249
|
+
if lval is None:
|
|
250
|
+
lval = unwrapped()
|
|
251
|
+
setattr(target, aname, lval)
|
|
252
|
+
self.__recursive_bind(lval, rval)
|
|
177
253
|
else:
|
|
178
254
|
if lval is None:
|
|
179
255
|
ahint = self.__deunionize(ahint)
|
|
@@ -262,12 +338,66 @@ class Configuration:
|
|
|
262
338
|
source = self.get(key)
|
|
263
339
|
if source is not None:
|
|
264
340
|
source_type = type(source)
|
|
265
|
-
if source_type
|
|
341
|
+
if source_type in (Configuration, dict):
|
|
266
342
|
return self.__recursive_bind(target, source)
|
|
343
|
+
elif source_type is list:
|
|
344
|
+
return self.__bind_list_source(target, key, source)
|
|
267
345
|
else:
|
|
268
|
-
|
|
269
|
-
|
|
346
|
+
# Scalar source: convert to matching typed attribute(s) on target
|
|
347
|
+
return self.__bind_scalar_to_target(target, key, source)
|
|
348
|
+
else:
|
|
349
|
+
return target
|
|
350
|
+
|
|
351
|
+
def __bind_list_source(self, target: object, key: str, source: list[Any]) -> Any:
|
|
352
|
+
"""Bind a Configuration- or dict-wrapped list source to *target*."""
|
|
353
|
+
if not hasattr(target, '__class__'):
|
|
270
354
|
return target
|
|
355
|
+
hints = get_type_hints(type(target))
|
|
356
|
+
raw_key = self.__scrub_key(key.replace(':', '__'))
|
|
357
|
+
for attr_name, ahint in hints.items():
|
|
358
|
+
if raw_key == self.__scrub_key(attr_name) or key.upper() == attr_name.upper():
|
|
359
|
+
element_type = ahint.__args__[0] if get_origin(ahint) is list else None
|
|
360
|
+
if element_type is not None and source:
|
|
361
|
+
lval = []
|
|
362
|
+
for e in source:
|
|
363
|
+
lval.append(self.__recursive_bind_type(element_type, e))
|
|
364
|
+
setattr(target, attr_name, lval)
|
|
365
|
+
return target
|
|
366
|
+
# taking object/untyped attr from normalized
|
|
367
|
+
for attr_name, ahint in hints.items():
|
|
368
|
+
if raw_key == self.__scrub_key(attr_name) or key.upper() == attr_name.upper():
|
|
369
|
+
if ahint is object:
|
|
370
|
+
setattr(target, attr_name, source)
|
|
371
|
+
return target
|
|
372
|
+
return target
|
|
373
|
+
|
|
374
|
+
def __bind_scalar_to_target(self, target: object, key: str, value: Any) -> Any:
|
|
375
|
+
"""Bind a scalar config value to the typed attribute on *target* matching the key."""
|
|
376
|
+
if not hasattr(target, '__class__'):
|
|
377
|
+
return target
|
|
378
|
+
hints = get_type_hints(type(target))
|
|
379
|
+
# Normalize / scrub exactly as :py:meth:`set` does so we can match back.
|
|
380
|
+
raw_key = self.__scrub_key(key.replace(':', '__'))
|
|
381
|
+
for attr_name, ahint in hints.items():
|
|
382
|
+
if raw_key == self.__scrub_key(attr_name):
|
|
383
|
+
converted = self.__convert_to_type(value, ahint)
|
|
384
|
+
setattr(target, attr_name, converted)
|
|
385
|
+
return target
|
|
386
|
+
|
|
387
|
+
@staticmethod
|
|
388
|
+
def __convert_to_type(value: Any, target_type: type) -> Any:
|
|
389
|
+
"""Convert *value* to *target_type* using primitive coercion rules."""
|
|
390
|
+
if target_type is float:
|
|
391
|
+
return float(value)
|
|
392
|
+
elif target_type is int:
|
|
393
|
+
return int(value)
|
|
394
|
+
elif target_type is str:
|
|
395
|
+
return str(value)
|
|
396
|
+
elif target_type is bool:
|
|
397
|
+
return bool(value)
|
|
398
|
+
else:
|
|
399
|
+
# Complex type or Any — nothing we can do for a scalar source
|
|
400
|
+
return value
|
|
271
401
|
|
|
272
402
|
def clear(self) -> None:
|
|
273
403
|
"""Clear all configuration data."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: appsettings2
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.9
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/ConfigurationProvider.py
RENAMED
|
File without changes
|
|
File without changes
|
{appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/JsonConfigurationProvider.py
RENAMED
|
File without changes
|
{appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/TomlConfigurationProvider.py
RENAMED
|
File without changes
|
{appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/YamlConfigurationProvider.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|