appsettings2 2.0.6__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.6/src/appsettings2.egg-info → appsettings2-2.0.9}/PKG-INFO +1 -1
- {appsettings2-2.0.6 → appsettings2-2.0.9}/pyproject.toml +1 -1
- {appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2/Configuration.py +159 -5
- {appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2/__init__.py +2 -2
- {appsettings2-2.0.6 → appsettings2-2.0.9/src/appsettings2.egg-info}/PKG-INFO +1 -1
- {appsettings2-2.0.6 → appsettings2-2.0.9}/LICENSE +0 -0
- {appsettings2-2.0.6 → appsettings2-2.0.9}/README.md +0 -0
- {appsettings2-2.0.6 → appsettings2-2.0.9}/setup.cfg +0 -0
- {appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2/ConfigurationBuilder.py +0 -0
- {appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2/ConfigurationException.py +0 -0
- {appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2/helpers.py +0 -0
- {appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2/providers/CommandLineConfigurationProvider.py +0 -0
- {appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2/providers/ConfigurationProvider.py +0 -0
- {appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2/providers/EnvironmentConfigurationProvider.py +0 -0
- {appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2/providers/JsonConfigurationProvider.py +0 -0
- {appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2/providers/TomlConfigurationProvider.py +0 -0
- {appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2/providers/YamlConfigurationProvider.py +0 -0
- {appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2/providers/__init__.py +0 -0
- {appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2/py.typed +0 -0
- {appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2.egg-info/SOURCES.txt +0 -0
- {appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2.egg-info/dependency_links.txt +0 -0
- {appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2.egg-info/requires.txt +0 -0
- {appsettings2-2.0.6 → 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)
|
|
@@ -139,7 +163,54 @@ class Configuration:
|
|
|
139
163
|
# NOTE: fget hint missing return spec, can't bind
|
|
140
164
|
continue
|
|
141
165
|
if rval is None:
|
|
166
|
+
if get_origin(ahint) is dict:
|
|
167
|
+
value_type = get_args(ahint)[1]
|
|
168
|
+
if isinstance(source, Configuration):
|
|
169
|
+
lval = dict[str, Any]()
|
|
170
|
+
for dk, dv in source.items():
|
|
171
|
+
lval[dk] = self.__recursive_bind_type(value_type, dv)
|
|
172
|
+
setattr(target, aname, lval)
|
|
173
|
+
continue
|
|
174
|
+
elif isinstance(source, dict):
|
|
175
|
+
if value_type in (str, int, float, bool, type(None)):
|
|
176
|
+
setattr(target, aname, source)
|
|
177
|
+
else:
|
|
178
|
+
lval = dict[str, Any]()
|
|
179
|
+
for dk, dv in source.items():
|
|
180
|
+
lval[dk] = self.__recursive_bind_type(value_type, dv)
|
|
181
|
+
setattr(target, aname, lval)
|
|
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
|
|
142
211
|
setattr(target, aname, None)
|
|
212
|
+
elif ahint is bool:
|
|
213
|
+
setattr(target, aname, bool(rval))
|
|
143
214
|
elif ahint is float:
|
|
144
215
|
setattr(target, aname, float(rval))
|
|
145
216
|
elif ahint is int:
|
|
@@ -148,8 +219,37 @@ class Configuration:
|
|
|
148
219
|
setattr(target, aname, str(rval))
|
|
149
220
|
elif isinstance(rval, Configuration):
|
|
150
221
|
if get_origin(ahint) is dict:
|
|
151
|
-
|
|
152
|
-
|
|
222
|
+
value_type = get_args(ahint)[1]
|
|
223
|
+
if value_type in (str, int, float, bool, type(None)):
|
|
224
|
+
lval = rval.to_dict()
|
|
225
|
+
setattr(target, aname, lval)
|
|
226
|
+
else:
|
|
227
|
+
lval = dict[str, Any]()
|
|
228
|
+
for dk, dv in rval.items():
|
|
229
|
+
lval[dk] = self.__recursive_bind_type(value_type, dv)
|
|
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)
|
|
153
253
|
else:
|
|
154
254
|
if lval is None:
|
|
155
255
|
ahint = self.__deunionize(ahint)
|
|
@@ -238,12 +338,66 @@ class Configuration:
|
|
|
238
338
|
source = self.get(key)
|
|
239
339
|
if source is not None:
|
|
240
340
|
source_type = type(source)
|
|
241
|
-
if source_type
|
|
341
|
+
if source_type in (Configuration, dict):
|
|
242
342
|
return self.__recursive_bind(target, source)
|
|
343
|
+
elif source_type is list:
|
|
344
|
+
return self.__bind_list_source(target, key, source)
|
|
243
345
|
else:
|
|
244
|
-
|
|
245
|
-
|
|
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__'):
|
|
246
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
|
|
247
401
|
|
|
248
402
|
def clear(self) -> None:
|
|
249
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.6 → appsettings2-2.0.9}/src/appsettings2/providers/ConfigurationProvider.py
RENAMED
|
File without changes
|
|
File without changes
|
{appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2/providers/JsonConfigurationProvider.py
RENAMED
|
File without changes
|
{appsettings2-2.0.6 → appsettings2-2.0.9}/src/appsettings2/providers/TomlConfigurationProvider.py
RENAMED
|
File without changes
|
{appsettings2-2.0.6 → 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
|