appsettings2 2.0.1__tar.gz → 2.0.7__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.1/src/appsettings2.egg-info → appsettings2-2.0.7}/PKG-INFO +7 -8
  2. {appsettings2-2.0.1 → appsettings2-2.0.7}/README.md +5 -5
  3. {appsettings2-2.0.1 → appsettings2-2.0.7}/pyproject.toml +3 -4
  4. {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/Configuration.py +69 -23
  5. {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/__init__.py +2 -2
  6. {appsettings2-2.0.1 → appsettings2-2.0.7/src/appsettings2.egg-info}/PKG-INFO +7 -8
  7. {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2.egg-info/requires.txt +1 -1
  8. {appsettings2-2.0.1 → appsettings2-2.0.7}/LICENSE +0 -0
  9. {appsettings2-2.0.1 → appsettings2-2.0.7}/setup.cfg +0 -0
  10. {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/ConfigurationBuilder.py +0 -0
  11. {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/ConfigurationException.py +0 -0
  12. {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/helpers.py +0 -0
  13. {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/CommandLineConfigurationProvider.py +0 -0
  14. {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/ConfigurationProvider.py +0 -0
  15. {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/EnvironmentConfigurationProvider.py +0 -0
  16. {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/JsonConfigurationProvider.py +0 -0
  17. {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/TomlConfigurationProvider.py +0 -0
  18. {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/YamlConfigurationProvider.py +0 -0
  19. {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/__init__.py +0 -0
  20. {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/py.typed +0 -0
  21. {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2.egg-info/SOURCES.txt +0 -0
  22. {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2.egg-info/dependency_links.txt +0 -0
  23. {appsettings2-2.0.1 → appsettings2-2.0.7}/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.1
3
+ Version: 2.0.7
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
@@ -32,7 +32,6 @@ Keywords: appsettings,configuration
32
32
  Classifier: Intended Audience :: Developers
33
33
  Classifier: Operating System :: OS Independent
34
34
  Classifier: Programming Language :: Python
35
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
36
35
  Requires-Python: >=3.11
37
36
  Description-Content-Type: text/markdown
38
37
  License-File: LICENSE
@@ -43,7 +42,7 @@ Requires-Dist: flake8; extra == "dev"
43
42
  Requires-Dist: flake8-pyproject; extra == "dev"
44
43
  Requires-Dist: mypy; extra == "dev"
45
44
  Requires-Dist: pep8-naming; extra == "dev"
46
- Requires-Dist: punit>=1.4.0; 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
@@ -132,12 +131,12 @@ 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))
@@ -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.toDictionary()
172
+ d = configuration.to_dict()
174
173
  print(d)
175
174
  ```
176
175
 
@@ -82,12 +82,12 @@ 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))
@@ -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.toDictionary()
123
+ d = configuration.to_dict()
124
124
  print(d)
125
125
  ```
126
126
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "appsettings2"
3
- version = "2.0.1"
3
+ version = "2.0.7"
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,8 +11,7 @@ license = {file = "LICENSE"}
11
11
  classifiers = [
12
12
  "Intended Audience :: Developers",
13
13
  "Operating System :: OS Independent",
14
- "Programming Language :: Python",
15
- "Topic :: Software Development :: Libraries :: Python Modules"
14
+ "Programming Language :: Python"
16
15
  ]
17
16
  requires-python = ">=3.11"
18
17
  dependencies = [
@@ -26,7 +25,7 @@ dev = [
26
25
  "flake8-pyproject",
27
26
  "mypy",
28
27
  "pep8-naming",
29
- "punit (>=1.4.0)",
28
+ "punit (>=1.4.9)",
30
29
  "twine",
31
30
  "types-PyYAML"
32
31
  ]
@@ -1,12 +1,14 @@
1
1
  # SPDX-FileCopyrightText: © 2025 Shaun Wilson
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
- import logging
4
+ from __future__ import annotations
5
+
5
6
  import json
7
+ import logging
6
8
  import re
7
9
  import types
8
- import typing
9
- from typing import Any, Iterator, 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
@@ -15,7 +17,7 @@ from .ConfigurationException import ConfigurationException
15
17
  class Configuration:
16
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."""
17
19
 
18
- __key_scrub_re: re.Pattern | None
20
+ __key_scrub_re: re.Pattern[str] | None
19
21
  __keys: dict[str, str]
20
22
  __normalize: bool
21
23
 
@@ -29,8 +31,11 @@ class Configuration:
29
31
  self.__keys = {}
30
32
  self.__logger = logging.getLogger('appsettings2')
31
33
  self.__normalize = normalize
32
- self.__key_scrub_re = None if not scrubkeys else re.compile(
33
- r'[^A-Za-z0-9_]', re.IGNORECASE | re.UNICODE)
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
+ )
34
39
 
35
40
  if TYPE_CHECKING:
36
41
 
@@ -38,7 +43,6 @@ class Configuration:
38
43
  """Tell type checkers that accessing any attribute returns any type."""
39
44
  ...
40
45
 
41
- # This tells mypy that setting any attribute is allowed
42
46
  def __setattr__(self, name: str, value: Any) -> None:
43
47
  """Tell type checkers that setting any attribute to any value is allowed."""
44
48
  ...
@@ -79,21 +83,27 @@ class Configuration:
79
83
  """Get the number of configuration keys."""
80
84
  return len(self.__keys)
81
85
 
82
- def __recursive_bind(self, target: object, source: 'Configuration|dict') -> Any:
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:
83
92
  if target is None:
84
93
  return None
85
94
  if hasattr(target, '__class__'):
86
- target_type_hints = typing.get_type_hints(
87
- getattr(target, '__class__'))
95
+ target_type_hints = get_type_hints(
96
+ getattr(target, '__class__')
97
+ )
88
98
  else:
89
- target_type_hints = typing.get_type_hints(target)
99
+ target_type_hints = get_type_hints(target)
90
100
  names = set(dir(target) | target_type_hints.keys())
91
101
  for aname in names:
92
102
  if aname.startswith('_'):
93
103
  continue
94
104
  lval = None if not hasattr(
95
105
  target, aname) else getattr(target, aname)
96
- if isinstance(lval, types.FunctionType) or isinstance(lval, types.MethodType):
106
+ if isinstance(lval, (types.FunctionType, types.MethodType)):
97
107
  continue
98
108
  ahint = target_type_hints.get(aname)
99
109
  rval = source.get(aname)
@@ -120,8 +130,8 @@ class Configuration:
120
130
  # non-list values cannot be merged
121
131
  raise Exception(
122
132
  f'Cannot bind `None` to attribute `{aname}`')
123
- phints = typing.get_type_hints(getattr(prop, 'fget'))
124
- if phints is None or (not issubclass(type(phints), dict)):
133
+ phints = get_type_hints(getattr(prop, 'fget'))
134
+ if phints is None:
125
135
  # NOTE: can't get hints from getter, can't bind
126
136
  continue
127
137
  ahint = phints.get('return')
@@ -129,6 +139,23 @@ class Configuration:
129
139
  # NOTE: fget hint missing return spec, can't bind
130
140
  continue
131
141
  if rval is None:
142
+ if get_origin(ahint) is dict:
143
+ value_type = get_args(ahint)[1]
144
+ if isinstance(source, Configuration):
145
+ lval = dict[str, Any]()
146
+ for dk, dv in source.items():
147
+ lval[dk] = self.__recursive_bind_type(value_type, dv)
148
+ setattr(target, aname, lval)
149
+ continue
150
+ elif isinstance(source, dict):
151
+ if value_type in (str, int, float, bool, type(None)):
152
+ setattr(target, aname, source)
153
+ else:
154
+ lval = dict[str, Any]()
155
+ for dk, dv in source.items():
156
+ lval[dk] = self.__recursive_bind_type(value_type, dv)
157
+ setattr(target, aname, lval)
158
+ continue
132
159
  setattr(target, aname, None)
133
160
  elif ahint is float:
134
161
  setattr(target, aname, float(rval))
@@ -137,23 +164,42 @@ class Configuration:
137
164
  elif ahint is str:
138
165
  setattr(target, aname, str(rval))
139
166
  elif isinstance(rval, Configuration):
140
- if typing.get_origin(ahint) is dict:
141
- lval = rval.toDictionary()
142
- setattr(target, aname, lval)
167
+ if get_origin(ahint) is dict:
168
+ value_type = get_args(ahint)[1]
169
+ if value_type in (str, int, float, bool, type(None)):
170
+ lval = rval.to_dict()
171
+ setattr(target, aname, lval)
172
+ else:
173
+ lval = dict[str, Any]()
174
+ for dk, dv in rval.items():
175
+ lval[dk] = self.__recursive_bind_type(value_type, dv)
176
+ setattr(target, aname, lval)
143
177
  else:
144
178
  if lval is None:
179
+ ahint = self.__deunionize(ahint)
145
180
  lval = ahint()
146
181
  setattr(target, aname, lval)
147
182
  self.__recursive_bind(lval, rval)
148
- elif typing.get_origin(ahint) is list:
183
+ elif get_origin(ahint) is list:
149
184
  element_type = ahint.__args__[0]
150
185
  if lval is None:
186
+ ahint = self.__deunionize(ahint)
151
187
  lval = ahint()
152
188
  setattr(target, aname, lval)
153
189
  for e in rval:
154
190
  lval.append(
155
191
  self.__recursive_bind_type(element_type, e)
156
192
  )
193
+ elif get_origin(ahint) is set:
194
+ element_type = ahint.__args__[0]
195
+ if lval is None:
196
+ ahint = self.__deunionize(ahint)
197
+ lval = ahint()
198
+ setattr(target, aname, lval)
199
+ for e in rval:
200
+ lval.add(
201
+ self.__recursive_bind_type(element_type, e)
202
+ )
157
203
  else:
158
204
  setattr(target, aname, rval)
159
205
  return target
@@ -167,7 +213,7 @@ class Configuration:
167
213
  return int(source)
168
214
  elif element_type is str:
169
215
  return str(source)
170
- elif isinstance(source, Configuration | dict):
216
+ elif isinstance(source, (Configuration, dict)):
171
217
  v = element_type()
172
218
  return self.__recursive_bind(v, source)
173
219
  else:
@@ -183,7 +229,7 @@ class Configuration:
183
229
  'NFKC',
184
230
  key))
185
231
 
186
- def __scrub_uc(self, m: re.Match) -> str:
232
+ def __scrub_uc(self, m: re.Match[str]) -> str:
187
233
  match unicodedata.category(m[0]):
188
234
  case 'Lu' | 'Ll' | 'Lt' | 'Lm' | 'Lo' | 'Nl' | 'Mn' | 'Mc' | 'Nd' | 'Pc':
189
235
  return m[0]
@@ -230,7 +276,7 @@ class Configuration:
230
276
  delattr(self, t[1])
231
277
 
232
278
  @staticmethod
233
- def from_dict(source: dict, normalize: bool = False, scrubkeys: bool = False) -> 'Configuration':
279
+ def from_dict(source: dict[str, Any], normalize: bool = False, scrubkeys: bool = False) -> 'Configuration':
234
280
  """
235
281
  Construct a :py:class:`~appsettings2.Configuration` instance from the supplied dictionary `source`.
236
282
 
@@ -359,12 +405,12 @@ class Configuration:
359
405
  for k in self.__keys.values():
360
406
  v = getattr(self, self.__scrub_key(k))
361
407
  if isinstance(v, Configuration):
362
- result[k] = v.toDictionary()
408
+ result[k] = v.to_dict()
363
409
  elif issubclass(type(v), list):
364
410
  tmp = []
365
411
  for e in v:
366
412
  if isinstance(e, Configuration):
367
- tmp.append(e.toDictionary())
413
+ tmp.append(e.to_dict())
368
414
  else:
369
415
  tmp.append(e)
370
416
  result[k] = tmp
@@ -10,8 +10,8 @@ from . import helpers
10
10
  from . import providers
11
11
 
12
12
 
13
- __version__ = '2.0.1'
14
- __commit__ = '0c59d3c'
13
+ __version__ = '2.0.7'
14
+ __commit__ = '62b357b'
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.1
3
+ Version: 2.0.7
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
@@ -32,7 +32,6 @@ Keywords: appsettings,configuration
32
32
  Classifier: Intended Audience :: Developers
33
33
  Classifier: Operating System :: OS Independent
34
34
  Classifier: Programming Language :: Python
35
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
36
35
  Requires-Python: >=3.11
37
36
  Description-Content-Type: text/markdown
38
37
  License-File: LICENSE
@@ -43,7 +42,7 @@ Requires-Dist: flake8; extra == "dev"
43
42
  Requires-Dist: flake8-pyproject; extra == "dev"
44
43
  Requires-Dist: mypy; extra == "dev"
45
44
  Requires-Dist: pep8-naming; extra == "dev"
46
- Requires-Dist: punit>=1.4.0; 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
@@ -132,12 +131,12 @@ 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))
@@ -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.toDictionary()
172
+ d = configuration.to_dict()
174
173
  print(d)
175
174
  ```
176
175
 
@@ -6,6 +6,6 @@ flake8
6
6
  flake8-pyproject
7
7
  mypy
8
8
  pep8-naming
9
- punit>=1.4.0
9
+ punit>=1.4.9
10
10
  twine
11
11
  types-PyYAML
File without changes
File without changes