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.
Files changed (26) hide show
  1. {appsettings2-1.3.7/src/appsettings2.egg-info → appsettings2-2.0.6}/PKG-INFO +19 -20
  2. {appsettings2-1.3.7 → appsettings2-2.0.6}/README.md +13 -13
  3. {appsettings2-1.3.7 → appsettings2-2.0.6}/pyproject.toml +13 -7
  4. {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/Configuration.py +141 -84
  5. {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/ConfigurationBuilder.py +59 -27
  6. appsettings2-2.0.6/src/appsettings2/ConfigurationException.py +12 -0
  7. {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/__init__.py +8 -5
  8. appsettings2-2.0.6/src/appsettings2/helpers.py +44 -0
  9. {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/providers/CommandLineConfigurationProvider.py +8 -8
  10. appsettings2-2.0.6/src/appsettings2/providers/ConfigurationProvider.py +20 -0
  11. {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/providers/EnvironmentConfigurationProvider.py +3 -4
  12. {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/providers/JsonConfigurationProvider.py +29 -27
  13. {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/providers/TomlConfigurationProvider.py +27 -26
  14. {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/providers/YamlConfigurationProvider.py +30 -25
  15. {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/providers/__init__.py +1 -0
  16. {appsettings2-1.3.7 → appsettings2-2.0.6/src/appsettings2.egg-info}/PKG-INFO +19 -20
  17. {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2.egg-info/requires.txt +4 -1
  18. appsettings2-1.3.7/src/appsettings2/ConfigurationException.py +0 -10
  19. appsettings2-1.3.7/src/appsettings2/helpers.py +0 -29
  20. appsettings2-1.3.7/src/appsettings2/providers/ConfigurationProvider.py +0 -17
  21. {appsettings2-1.3.7 → appsettings2-2.0.6}/LICENSE +0 -0
  22. {appsettings2-1.3.7 → appsettings2-2.0.6}/setup.cfg +0 -0
  23. {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2/py.typed +0 -0
  24. {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2.egg-info/SOURCES.txt +0 -0
  25. {appsettings2-1.3.7 → appsettings2-2.0.6}/src/appsettings2.egg-info/dependency_links.txt +0 -0
  26. {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: 1.3.7
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 :: 3.11
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: punit>=1.4.0; extra == "dev"
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
- .addProvider(JsonConfigurationProvider(f'appsettings.json'))\
73
- .addProvider(JsonConfigurationProvider(f'appsettings.Development.json', required=False))\
74
- .addProvider(EnvironmentConfigurationProvider())\
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
- .addProvider(JsonConfigurationProvider(json=json))
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.toDictionary()
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 `populateConfiguration(...)`.
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 populateConfiguration(self, configuration:Configuration) -> None:
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.getConfiguration()
208
+ config = appsettings2.get_configuration()
210
209
  ```
211
210
 
212
- You can read more about `getConfiguration` in [the docs](https://appsettings2.readthedocs.io/en/latest/ref/helpers/getConfiguration.html).
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
- .addProvider(JsonConfigurationProvider(f'appsettings.json'))\
23
- .addProvider(JsonConfigurationProvider(f'appsettings.Development.json', required=False))\
24
- .addProvider(EnvironmentConfigurationProvider())\
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
- .addProvider(JsonConfigurationProvider(json=json))
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.toDictionary()
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 `populateConfiguration(...)`.
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 populateConfiguration(self, configuration:Configuration) -> None:
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.getConfiguration()
159
+ config = appsettings2.get_configuration()
160
160
  ```
161
161
 
162
- You can read more about `getConfiguration` in [the docs](https://appsettings2.readthedocs.io/en/latest/ref/helpers/getConfiguration.html).
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 = "1.3.7"
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 :: 3.11",
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
- "punit (>=1.4.0)",
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 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, 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, *, normalize:bool = False, scrubkeys:bool = False):
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 = None if not scrubkeys else re.compile(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
+ )
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
- def __delitem__(self, key:str) -> None:
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
- Gets the configuration data associated with the specified `key`.
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 __recursiveBind(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:
75
92
  if target is None:
76
93
  return None
77
94
  if hasattr(target, '__class__'):
78
- targetTypeHints = typing.get_type_hints(getattr(target, '__class__'))
95
+ target_type_hints = get_type_hints(
96
+ getattr(target, '__class__')
97
+ )
79
98
  else:
80
- targetTypeHints = typing.get_type_hints(target)
81
- names = set(dir(target) | targetTypeHints.keys())
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(target, aname) else getattr(target, aname)
86
- if isinstance(lval, types.FunctionType) or isinstance(lval, types.MethodType):
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 = targetTypeHints.get(aname)
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(f'Failed to bind {aname}', exc_info=True)
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(f'Cannot bind `None` to attribute `{aname}`')
111
- phints = typing.get_type_hints(getattr(prop, 'fget'))
112
- if phints is None or (not issubclass(type(phints), dict)):
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 typing.get_origin(ahint) is dict:
129
- lval = rval.toDictionary()
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.__recursiveBind(lval, rval)
136
- elif typing.get_origin(ahint) is list:
137
- elementType = ahint.__args__[0]
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.__recursiveBindType(elementType, e)
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 __recursiveBindType(self, elementType:type, source:Any) -> Any:
150
- if isinstance(source, elementType):
183
+ def __recursive_bind_type(self, element_type: type, source: Any) -> Any:
184
+ if isinstance(source, element_type):
151
185
  return source
152
- elif elementType is float:
186
+ elif element_type is float:
153
187
  return float(source)
154
- elif elementType is int:
188
+ elif element_type is int:
155
189
  return int(source)
156
- elif elementType is str:
190
+ elif element_type is str:
157
191
  return str(source)
158
- elif isinstance(source, Configuration | dict):
159
- v = elementType()
160
- return self.__recursiveBind(v, source)
192
+ elif isinstance(source, (Configuration, dict)):
193
+ v = element_type()
194
+ return self.__recursive_bind(v, source)
161
195
  else:
162
- raise ConfigurationException(f'Recursive bind to type `{elementType}` from `{type(source)}` is not supported.')
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
- self.__scrub_uc,
169
- unicodedata.normalize(
170
- 'NFKC',
171
- key))
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
- return json.dumps(self.toDictionary())
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.__recursiveBind(target, 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
- sourceType = type(source)
203
- if sourceType is Configuration or sourceType is dict:
204
- return self.__recursiveBind(target, source)
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(f'Bind of source type `{type(source)}` is not supported.')
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 fromDictionary(source:dict, *, normalize:bool = False, scrubkeys:bool = False) -> 'Configuration':
255
+ def from_dict(source: dict[str, Any], normalize: bool = False, scrubkeys: bool = False) -> 'Configuration':
216
256
  """
217
- Constructs a :py:class:`~appsettings2.Configuration` instance from the supplied dictionary `source`.
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(normalize=normalize, scrubkeys=scrubkeys)
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.fromDictionary(v, normalize=normalize, scrubkeys=scrubkeys)
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
- def get(self, key:str, default:Any = None) -> Any:
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
- Gets the configuration data associated with the specified `key`.
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
- Sets the configuration data for the specified `key`.
322
+ Set the configuration value for a configuration key.
274
323
 
275
- :param key: The key to associate the configuration data.
276
- :param value: The configuration data so be associated with `key`.
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=(None != self.__key_scrub_re))
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=(None != self.__key_scrub_re))
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.fromDictionary(value, normalize=self.__normalize, scrubkeys=self.__key_scrub_re is not None)
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.fromDictionary(e, normalize=self.__normalize, scrubkeys=self.__key_scrub_re is not None))
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 toDictionary(self) -> dict[str,Any]:
374
+ def to_dict(self) -> dict[str, Any]:
322
375
  """
323
- Creates a dictionary from the `Configuration` object.
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.toDictionary()
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.toDictionary())
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))