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.
Files changed (23) hide show
  1. {appsettings2-2.0.7/src/appsettings2.egg-info → appsettings2-2.0.9}/PKG-INFO +1 -1
  2. {appsettings2-2.0.7 → appsettings2-2.0.9}/pyproject.toml +1 -1
  3. {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/Configuration.py +133 -3
  4. {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/__init__.py +2 -2
  5. {appsettings2-2.0.7 → appsettings2-2.0.9/src/appsettings2.egg-info}/PKG-INFO +1 -1
  6. {appsettings2-2.0.7 → appsettings2-2.0.9}/LICENSE +0 -0
  7. {appsettings2-2.0.7 → appsettings2-2.0.9}/README.md +0 -0
  8. {appsettings2-2.0.7 → appsettings2-2.0.9}/setup.cfg +0 -0
  9. {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/ConfigurationBuilder.py +0 -0
  10. {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/ConfigurationException.py +0 -0
  11. {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/helpers.py +0 -0
  12. {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/CommandLineConfigurationProvider.py +0 -0
  13. {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/ConfigurationProvider.py +0 -0
  14. {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/EnvironmentConfigurationProvider.py +0 -0
  15. {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/JsonConfigurationProvider.py +0 -0
  16. {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/TomlConfigurationProvider.py +0 -0
  17. {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/YamlConfigurationProvider.py +0 -0
  18. {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/providers/__init__.py +0 -0
  19. {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2/py.typed +0 -0
  20. {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2.egg-info/SOURCES.txt +0 -0
  21. {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2.egg-info/dependency_links.txt +0 -0
  22. {appsettings2-2.0.7 → appsettings2-2.0.9}/src/appsettings2.egg-info/requires.txt +0 -0
  23. {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.7
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.7"
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 is Configuration or source_type is dict:
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
- raise ConfigurationException(
269
- f'Bind of source type `{type(source)}` is not supported.')
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."""
@@ -10,8 +10,8 @@ from . import helpers
10
10
  from . import providers
11
11
 
12
12
 
13
- __version__ = '2.0.7'
14
- __commit__ = '62b357b'
13
+ __version__ = '2.0.9'
14
+ __commit__ = 'b510516'
15
15
  __all__ = [
16
16
  '__version__', '__commit__',
17
17
  'Configuration',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appsettings2
3
- Version: 2.0.7
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