appsettings2 2.0.10__tar.gz → 2.1.0__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.10/src/appsettings2.egg-info → appsettings2-2.1.0}/PKG-INFO +4 -25
- {appsettings2-2.0.10 → appsettings2-2.1.0}/pyproject.toml +7 -7
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/Configuration.py +160 -37
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/ConfigurationBuilder.py +48 -4
- appsettings2-2.1.0/src/appsettings2/ConfigurationWatcher.py +375 -0
- appsettings2-2.1.0/src/appsettings2/SKILL.md +475 -0
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/__init__.py +7 -3
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/helpers.py +4 -2
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/providers/ConfigurationProvider.py +2 -1
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/providers/JsonConfigurationProvider.py +12 -4
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/providers/TomlConfigurationProvider.py +13 -5
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/providers/YamlConfigurationProvider.py +14 -6
- {appsettings2-2.0.10 → appsettings2-2.1.0/src/appsettings2.egg-info}/PKG-INFO +4 -25
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2.egg-info/SOURCES.txt +2 -0
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2.egg-info/requires.txt +2 -2
- {appsettings2-2.0.10 → appsettings2-2.1.0}/LICENSE +0 -0
- {appsettings2-2.0.10 → appsettings2-2.1.0}/README.md +0 -0
- {appsettings2-2.0.10 → appsettings2-2.1.0}/setup.cfg +0 -0
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/ConfigurationException.py +0 -0
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/providers/CommandLineConfigurationProvider.py +1 -1
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/providers/EnvironmentConfigurationProvider.py +0 -0
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/providers/__init__.py +0 -0
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/py.typed +0 -0
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2.egg-info/dependency_links.txt +0 -0
- {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2.egg-info/top_level.txt +0 -0
|
@@ -1,30 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: appsettings2
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
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
|
-
License: MIT
|
|
7
|
-
|
|
8
|
-
© 2025 Shaun Wilson
|
|
9
|
-
|
|
10
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
-
in the Software without restriction, including without limitation the rights
|
|
13
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
-
furnished to do so, subject to the following conditions:
|
|
16
|
-
|
|
17
|
-
The above copyright notice and this permission notice shall be included in all
|
|
18
|
-
copies or substantial portions of the Software.
|
|
19
|
-
|
|
20
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
-
SOFTWARE.
|
|
27
|
-
|
|
6
|
+
License-Expression: MIT
|
|
28
7
|
Project-URL: Documentation, https://appsettings2.readthedocs.io/
|
|
29
8
|
Project-URL: Homepage, https://github.com/wilson0x4d/appsettings2
|
|
30
9
|
Project-URL: Repository, https://github.com/wilson0x4d/appsettings2.git
|
|
@@ -35,14 +14,14 @@ Classifier: Programming Language :: Python
|
|
|
35
14
|
Requires-Python: >=3.11
|
|
36
15
|
Description-Content-Type: text/markdown
|
|
37
16
|
License-File: LICENSE
|
|
38
|
-
Requires-Dist: pyyaml>=6.0.
|
|
17
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
39
18
|
Provides-Extra: dev
|
|
40
19
|
Requires-Dist: coverage; extra == "dev"
|
|
41
20
|
Requires-Dist: flake8; extra == "dev"
|
|
42
21
|
Requires-Dist: flake8-pyproject; extra == "dev"
|
|
43
22
|
Requires-Dist: mypy; extra == "dev"
|
|
44
23
|
Requires-Dist: pep8-naming; extra == "dev"
|
|
45
|
-
Requires-Dist: punit>=1.
|
|
24
|
+
Requires-Dist: punit>=1.12.5; extra == "dev"
|
|
46
25
|
Requires-Dist: twine; extra == "dev"
|
|
47
26
|
Requires-Dist: types-PyYAML; extra == "dev"
|
|
48
27
|
Dynamic: license-file
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "appsettings2"
|
|
3
|
-
version = "2.0
|
|
3
|
+
version = "2.1.0"
|
|
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 = [
|
|
7
7
|
{ name="Shaun Wilson", email="mrshaunwilson@msn.com" }
|
|
8
8
|
]
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
license =
|
|
10
|
+
license = "MIT"
|
|
11
11
|
classifiers = [
|
|
12
12
|
"Intended Audience :: Developers",
|
|
13
13
|
"Operating System :: OS Independent",
|
|
@@ -15,7 +15,7 @@ classifiers = [
|
|
|
15
15
|
]
|
|
16
16
|
requires-python = ">=3.11"
|
|
17
17
|
dependencies = [
|
|
18
|
-
"pyyaml>=6.0.
|
|
18
|
+
"pyyaml>=6.0.3"
|
|
19
19
|
]
|
|
20
20
|
|
|
21
21
|
[project.optional-dependencies]
|
|
@@ -25,7 +25,7 @@ dev = [
|
|
|
25
25
|
"flake8-pyproject",
|
|
26
26
|
"mypy",
|
|
27
27
|
"pep8-naming",
|
|
28
|
-
"punit (>=1.
|
|
28
|
+
"punit (>=1.12.5)",
|
|
29
29
|
"twine",
|
|
30
30
|
"types-PyYAML"
|
|
31
31
|
]
|
|
@@ -48,11 +48,11 @@ exclude = [
|
|
|
48
48
|
]
|
|
49
49
|
|
|
50
50
|
[tool.setuptools.package-data]
|
|
51
|
-
"appsettings2" = ["py.typed"]
|
|
51
|
+
"appsettings2" = ["py.typed", "SKILL.md"]
|
|
52
52
|
|
|
53
53
|
[tool.flake8]
|
|
54
54
|
max-line-length = 120
|
|
55
55
|
ignore = ["D100","E501"]
|
|
56
|
-
per-file-
|
|
57
|
-
"tests/*.py:D012",
|
|
56
|
+
per-file-ignores = [
|
|
57
|
+
"tests/*.py:D012,E306",
|
|
58
58
|
]
|
|
@@ -7,26 +7,42 @@ import json
|
|
|
7
7
|
import logging
|
|
8
8
|
import re
|
|
9
9
|
import types
|
|
10
|
-
from types import NoneType
|
|
11
|
-
from typing import Any, Iterator, Union, TYPE_CHECKING, get_args, get_origin, get_type_hints
|
|
12
10
|
import unicodedata
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from types import NoneType
|
|
13
|
+
from typing import Any, Callable, Iterator, Union, TYPE_CHECKING, get_args, get_origin, get_type_hints
|
|
13
14
|
|
|
14
15
|
from .ConfigurationException import ConfigurationException
|
|
15
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .ConfigurationWatcher import ConfigurationWatcher
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ChangeType(str, Enum):
|
|
22
|
+
CHANGING = 'changing'
|
|
23
|
+
CHANGED = 'changed'
|
|
24
|
+
CLOSED = 'closed'
|
|
25
|
+
|
|
16
26
|
|
|
17
27
|
class Configuration:
|
|
18
28
|
"""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
29
|
|
|
30
|
+
__handlers: list[Callable[[Configuration, ChangeType, str | None], None]]
|
|
20
31
|
__key_scrub_re: re.Pattern[str] | None
|
|
21
32
|
__keys: dict[str, str]
|
|
22
33
|
__normalize: bool
|
|
34
|
+
__watcher: ConfigurationWatcher | None
|
|
23
35
|
|
|
24
|
-
|
|
36
|
+
disable_events: bool
|
|
37
|
+
|
|
38
|
+
def __init__(self, normalize: bool = False, scrubkeys: bool = False, disable_events: bool | None = None, watcher: ConfigurationWatcher | None = None) -> None:
|
|
25
39
|
"""
|
|
26
40
|
Initialize *Configuration* instance.
|
|
27
41
|
|
|
28
42
|
: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.
|
|
29
43
|
:param scrubkeys: Option indicating whether or not attribute names should be scrubbed to be compatible with the Python lexer, defaults to False.
|
|
44
|
+
:param disable_events: Option indicating whether change events should be emitted, defaults to False.
|
|
45
|
+
:param watcher: Optional :py:class:`~appsettings2.ConfigurationWatcher` to associate with this configuration.
|
|
30
46
|
"""
|
|
31
47
|
self.__keys = {}
|
|
32
48
|
self.__logger = logging.getLogger('appsettings2')
|
|
@@ -36,6 +52,11 @@ class Configuration:
|
|
|
36
52
|
if not scrubkeys
|
|
37
53
|
else re.compile(r'[^A-Za-z0-9_]', re.IGNORECASE | re.UNICODE)
|
|
38
54
|
)
|
|
55
|
+
self.disable_events = disable_events is True
|
|
56
|
+
self.__handlers = []
|
|
57
|
+
self.__watcher = watcher
|
|
58
|
+
if self.__watcher is not None:
|
|
59
|
+
self.__watcher.watch_handler = self.__on_watch_event
|
|
39
60
|
|
|
40
61
|
if TYPE_CHECKING:
|
|
41
62
|
|
|
@@ -47,13 +68,76 @@ class Configuration:
|
|
|
47
68
|
"""Tell type checkers that setting any attribute to any value is allowed."""
|
|
48
69
|
...
|
|
49
70
|
|
|
71
|
+
def __del__(self) -> None:
|
|
72
|
+
self.close()
|
|
73
|
+
|
|
74
|
+
def __on_changing(self, key: str | None) -> None:
|
|
75
|
+
if self.disable_events:
|
|
76
|
+
return
|
|
77
|
+
key = self.__normalize_key(key)
|
|
78
|
+
for entry in self.__handlers:
|
|
79
|
+
entry(self, ChangeType.CHANGING, key)
|
|
80
|
+
|
|
81
|
+
def __on_changed(self, key: str | None) -> None:
|
|
82
|
+
if self.disable_events:
|
|
83
|
+
return
|
|
84
|
+
key = self.__normalize_key(key)
|
|
85
|
+
for entry in self.__handlers:
|
|
86
|
+
entry(self, ChangeType.CHANGED, key)
|
|
87
|
+
|
|
88
|
+
def __on_close(self, key: str | None) -> None:
|
|
89
|
+
if self.disable_events:
|
|
90
|
+
return
|
|
91
|
+
key = self.__normalize_key(key)
|
|
92
|
+
for entry in self.__handlers:
|
|
93
|
+
entry(self, ChangeType.CLOSED, None)
|
|
94
|
+
self.__handlers.clear()
|
|
95
|
+
|
|
96
|
+
def __on_watch_event(self, filepath: str, watch_event_type: str, configuration: Configuration | None = None) -> None:
|
|
97
|
+
if watch_event_type in ('c', 'm') and configuration is not None:
|
|
98
|
+
self.merge(configuration)
|
|
99
|
+
|
|
100
|
+
def __add_child_change_handler(self, child_key: str) -> None:
|
|
101
|
+
if self.disable_events:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
def child_change_handler(configuration: Configuration, change_type: ChangeType, key: str | None) -> None:
|
|
105
|
+
if change_type is ChangeType.CHANGING:
|
|
106
|
+
self.__on_changing(f'{child_key}.{key}')
|
|
107
|
+
elif change_type is ChangeType.CHANGED:
|
|
108
|
+
self.__on_changed(f'{child_key}.{key}')
|
|
109
|
+
child_change_handler.__dict__['_auto_wired'] = True
|
|
110
|
+
child_change_handler.__dict__['_auto_wired_child_key'] = child_key
|
|
111
|
+
child_change_handler.__dict__['_auto_wired_parent'] = self
|
|
112
|
+
self[child_key].add_change_handler(child_change_handler)
|
|
113
|
+
|
|
114
|
+
def __remove_child_change_handler(self, child_key: str) -> None:
|
|
115
|
+
child = getattr(self, child_key, None)
|
|
116
|
+
if child is None:
|
|
117
|
+
return
|
|
118
|
+
child_handlers = child.__handlers
|
|
119
|
+
for i in range(len(child_handlers) - 1, -1, -1):
|
|
120
|
+
handler = child_handlers[i]
|
|
121
|
+
d = getattr(handler, '__dict__', {})
|
|
122
|
+
if d.get('_auto_wired') and d.get('_auto_wired_child_key') == child_key:
|
|
123
|
+
child_handlers.pop(i)
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
def __normalize_key(self, key: str | None) -> str | None:
|
|
127
|
+
return None if key is None else key.replace('__', '.').replace(':', '.')
|
|
128
|
+
|
|
50
129
|
def __delitem__(self, key: str) -> None:
|
|
51
130
|
"""Delete the specified configuration key."""
|
|
52
131
|
key = key.upper()
|
|
53
132
|
k = self.__keys.get(key)
|
|
54
133
|
if k is not None:
|
|
134
|
+
value = getattr(self, k)
|
|
135
|
+
self.__on_changing(key)
|
|
136
|
+
if isinstance(value, Configuration):
|
|
137
|
+
self.__remove_child_change_handler(k)
|
|
55
138
|
delattr(self, k)
|
|
56
139
|
self.__keys.pop(key)
|
|
140
|
+
self.__on_changed(key)
|
|
57
141
|
|
|
58
142
|
def __getitem__(self, key: str) -> Any:
|
|
59
143
|
"""
|
|
@@ -320,34 +404,6 @@ class Configuration:
|
|
|
320
404
|
"""Render the configuration as a JSON string."""
|
|
321
405
|
return json.dumps(self.to_dict(), indent=None, ensure_ascii=False)
|
|
322
406
|
|
|
323
|
-
def bind(self, target: object, key: str | None = None) -> Any:
|
|
324
|
-
"""
|
|
325
|
-
Binds the configuration values into the target object.
|
|
326
|
-
|
|
327
|
-
Can optionally specify a configuration key to bind from.
|
|
328
|
-
|
|
329
|
-
:param target: The object to bind configuration data into.
|
|
330
|
-
:param key: An optional confguration key to bind to, defaults to None which binds to the configuration root.
|
|
331
|
-
:return: The original `target` object, modified in-place.
|
|
332
|
-
"""
|
|
333
|
-
if target is None:
|
|
334
|
-
raise ConfigurationException('Missing required argument: target')
|
|
335
|
-
if key is None:
|
|
336
|
-
return self.__recursive_bind(target, self)
|
|
337
|
-
else:
|
|
338
|
-
source = self.get(key)
|
|
339
|
-
if source is not None:
|
|
340
|
-
source_type = type(source)
|
|
341
|
-
if source_type in (Configuration, dict):
|
|
342
|
-
return self.__recursive_bind(target, source)
|
|
343
|
-
elif source_type is list:
|
|
344
|
-
return self.__bind_list_source(target, key, source)
|
|
345
|
-
else:
|
|
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
407
|
def __bind_list_source(self, target: object, key: str, source: list[Any]) -> Any:
|
|
352
408
|
"""Bind a Configuration- or dict-wrapped list source to *target*."""
|
|
353
409
|
if not hasattr(target, '__class__'):
|
|
@@ -399,29 +455,70 @@ class Configuration:
|
|
|
399
455
|
# Complex type or Any — nothing we can do for a scalar source
|
|
400
456
|
return value
|
|
401
457
|
|
|
458
|
+
def bind(self, target: object, key: str | None = None) -> Any:
|
|
459
|
+
"""
|
|
460
|
+
Binds the configuration values into the target object.
|
|
461
|
+
|
|
462
|
+
Can optionally specify a configuration key to bind from.
|
|
463
|
+
|
|
464
|
+
:param target: The object to bind configuration data into.
|
|
465
|
+
:param key: An optional confguration key to bind to, defaults to None which binds to the configuration root.
|
|
466
|
+
:return: The original `target` object, modified in-place.
|
|
467
|
+
"""
|
|
468
|
+
if target is None:
|
|
469
|
+
raise ConfigurationException('Missing required argument: target')
|
|
470
|
+
if key is None:
|
|
471
|
+
return self.__recursive_bind(target, self)
|
|
472
|
+
else:
|
|
473
|
+
source = self.get(key)
|
|
474
|
+
if source is not None:
|
|
475
|
+
source_type = type(source)
|
|
476
|
+
if source_type in (Configuration, dict):
|
|
477
|
+
return self.__recursive_bind(target, source)
|
|
478
|
+
elif source_type is list:
|
|
479
|
+
return self.__bind_list_source(target, key, source)
|
|
480
|
+
else:
|
|
481
|
+
# Scalar source: convert to matching typed attribute(s) on target
|
|
482
|
+
return self.__bind_scalar_to_target(target, key, source)
|
|
483
|
+
else:
|
|
484
|
+
return target
|
|
485
|
+
|
|
486
|
+
def close(self) -> None:
|
|
487
|
+
self.__on_close(None)
|
|
488
|
+
|
|
489
|
+
def add_change_handler(self, handler: Callable[["Configuration", ChangeType, str | None], None]) -> None:
|
|
490
|
+
self.__handlers.append(handler)
|
|
491
|
+
|
|
492
|
+
def remove_change_handler(self, handler: Callable[["Configuration", ChangeType, str | None], None]) -> None:
|
|
493
|
+
for i in range(len(self.__handlers)):
|
|
494
|
+
if self.__handlers[i] is handler:
|
|
495
|
+
self.__handlers.pop(i)
|
|
496
|
+
break
|
|
497
|
+
|
|
402
498
|
def clear(self) -> None:
|
|
403
499
|
"""Clear all configuration data."""
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
500
|
+
keys = list(self.__keys.keys())
|
|
501
|
+
for uk in keys:
|
|
502
|
+
del self[uk]
|
|
407
503
|
|
|
408
504
|
@staticmethod
|
|
409
|
-
def from_dict(source: dict[str, Any], normalize: bool = False, scrubkeys: bool = False) ->
|
|
505
|
+
def from_dict(source: dict[str, Any], normalize: bool = False, scrubkeys: bool = False, disable_events: bool | None = None) -> Configuration:
|
|
410
506
|
"""
|
|
411
507
|
Construct a :py:class:`~appsettings2.Configuration` instance from the supplied dictionary `source`.
|
|
412
508
|
|
|
413
509
|
:param source: The dictionary object to populate from.
|
|
414
510
|
: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.
|
|
415
511
|
:param scrubkeys: Option indicating whether or not attribute names should be scrubbed to be compatible with the Python lexer, defaults to False.
|
|
512
|
+
:param disable_events: Option indicating whether change events should be emitted, defaults to False.
|
|
416
513
|
:return: A :py:class:`~appsettings2.Configuration` object derived from the `source` parameter.
|
|
417
514
|
"""
|
|
418
515
|
config: Configuration = Configuration(
|
|
419
|
-
normalize=normalize, scrubkeys=scrubkeys)
|
|
516
|
+
normalize=normalize, scrubkeys=scrubkeys, disable_events=disable_events)
|
|
420
517
|
for kvp in source.items():
|
|
421
518
|
v = kvp[1]
|
|
422
519
|
if issubclass(type(v), dict):
|
|
423
520
|
v = Configuration.from_dict(
|
|
424
|
-
v, normalize=normalize, scrubkeys=scrubkeys)
|
|
521
|
+
v, normalize=normalize, scrubkeys=scrubkeys, disable_events=disable_events)
|
|
425
522
|
config.set(kvp[0], v)
|
|
426
523
|
return config
|
|
427
524
|
|
|
@@ -465,6 +562,19 @@ class Configuration:
|
|
|
465
562
|
"""Get a list of all configuration keys."""
|
|
466
563
|
return list(self.__keys.values())
|
|
467
564
|
|
|
565
|
+
def merge(self, configuration: Configuration) -> None:
|
|
566
|
+
"""
|
|
567
|
+
Performs a shallow merge of the provided configuration object.
|
|
568
|
+
|
|
569
|
+
Keys present in ``configuration`` that do not already exist in ``self`` (or have differing values) are written to ``self``.
|
|
570
|
+
|
|
571
|
+
:param configuration: The object to merge.
|
|
572
|
+
"""
|
|
573
|
+
_sentinel = object()
|
|
574
|
+
for key in configuration.keys():
|
|
575
|
+
if self.get(key, _sentinel) != configuration[key]:
|
|
576
|
+
self[key] = configuration[key]
|
|
577
|
+
|
|
468
578
|
def pop(self, key: str) -> Any:
|
|
469
579
|
"""Delete a specific configuration key."""
|
|
470
580
|
value = self[key]
|
|
@@ -481,7 +591,11 @@ class Configuration:
|
|
|
481
591
|
if self.__normalize:
|
|
482
592
|
key = key.upper()
|
|
483
593
|
parts = key.replace(':', '__').split('__')
|
|
594
|
+
full_key_for_events = key.replace('__', '.').replace(':', '.')
|
|
595
|
+
self.__on_changing(full_key_for_events)
|
|
484
596
|
o = self
|
|
597
|
+
|
|
598
|
+
# Build intermediate hierarchy without emitting events
|
|
485
599
|
for i in range(len(parts) - 1):
|
|
486
600
|
if o == self:
|
|
487
601
|
k = self.__keys.get(parts[i].upper())
|
|
@@ -490,6 +604,7 @@ class Configuration:
|
|
|
490
604
|
self.__key_scrub_re is None))
|
|
491
605
|
setattr(o, self.__scrub_key(parts[i]), c)
|
|
492
606
|
self.__keys[parts[i].upper()] = parts[i]
|
|
607
|
+
o.__add_child_change_handler(parts[i])
|
|
493
608
|
o = c
|
|
494
609
|
else:
|
|
495
610
|
o = getattr(self, self.__scrub_key(k))
|
|
@@ -501,6 +616,8 @@ class Configuration:
|
|
|
501
616
|
o = c
|
|
502
617
|
else:
|
|
503
618
|
o = o.get(parts[i])
|
|
619
|
+
|
|
620
|
+
# Convert dict/list values
|
|
504
621
|
vtype = type(value)
|
|
505
622
|
if issubclass(vtype, dict):
|
|
506
623
|
value = Configuration.from_dict(
|
|
@@ -514,6 +631,7 @@ class Configuration:
|
|
|
514
631
|
else:
|
|
515
632
|
l.append(e)
|
|
516
633
|
value = l
|
|
634
|
+
|
|
517
635
|
key = parts[-1]
|
|
518
636
|
if o == self:
|
|
519
637
|
k = self.__keys.get(key.upper())
|
|
@@ -522,9 +640,14 @@ class Configuration:
|
|
|
522
640
|
else:
|
|
523
641
|
self.__keys[key.upper()] = key
|
|
524
642
|
setattr(self, self.__scrub_key(key), value)
|
|
643
|
+
if isinstance(value, Configuration):
|
|
644
|
+
self.__add_child_change_handler(key)
|
|
525
645
|
else:
|
|
526
646
|
o.set(key, value)
|
|
527
647
|
|
|
648
|
+
self.__on_changed(full_key_for_events)
|
|
649
|
+
|
|
650
|
+
|
|
528
651
|
def to_dict(self) -> dict[str, Any]:
|
|
529
652
|
"""
|
|
530
653
|
Create a dictionary from the `Configuration` object.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: © 2025 Shaun Wilson
|
|
2
2
|
# SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
+
from typing import Any, Optional, TypeAlias
|
|
5
|
+
|
|
4
6
|
from .Configuration import Configuration
|
|
5
7
|
from .ConfigurationException import ConfigurationException
|
|
6
8
|
from .providers import (
|
|
@@ -11,7 +13,6 @@ from .providers import (
|
|
|
11
13
|
TomlConfigurationProvider,
|
|
12
14
|
YamlConfigurationProvider,
|
|
13
15
|
)
|
|
14
|
-
from typing import Optional, TypeAlias
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
FileDescriptor: TypeAlias = int
|
|
@@ -22,19 +23,25 @@ class ConfigurationBuilder:
|
|
|
22
23
|
Build a :py:class:`~appsettings2.Configuration` object from one or more :py:class:`~appsettings2.providers.ConfigurationProvider` instances.
|
|
23
24
|
"""
|
|
24
25
|
|
|
26
|
+
__disable_events: bool | None
|
|
25
27
|
__normalize: bool
|
|
26
28
|
__providers: list[ConfigurationProvider]
|
|
29
|
+
__watcher: Any
|
|
27
30
|
|
|
28
|
-
def __init__(self, normalize: bool = False, scrubkeys: bool = False) -> None:
|
|
31
|
+
def __init__(self, normalize: bool = False, scrubkeys: bool = False, disable_events: bool | None = None, watcher: Any = None) -> None:
|
|
29
32
|
"""
|
|
30
33
|
Initialize *ConfigurationBuilder* instance.
|
|
31
34
|
|
|
32
35
|
: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.
|
|
33
36
|
:param scrubkeys: Option indicating whether or not attribute names should be scrubbed to be compatible with the Python lexer, defaults to False.
|
|
37
|
+
:param disable_events: Option indicating whether change events should be emitted, defaults to None.
|
|
38
|
+
:param watcher: Optional :py:class:`~appsettings2.ConfigurationWatcher` to associate with the built configuration.
|
|
34
39
|
"""
|
|
40
|
+
self.__disable_events = disable_events
|
|
35
41
|
self.__normalize = normalize
|
|
36
42
|
self.__scrubkeys = scrubkeys
|
|
37
43
|
self.__providers = []
|
|
44
|
+
self.__watcher = watcher
|
|
38
45
|
|
|
39
46
|
def add_provider(self, provider: ConfigurationProvider) -> 'ConfigurationBuilder':
|
|
40
47
|
"""
|
|
@@ -128,16 +135,53 @@ class ConfigurationBuilder:
|
|
|
128
135
|
addYaml = add_yaml
|
|
129
136
|
"""⚠️ DEPRECATED: use ``add_yaml(...)`` instead."""
|
|
130
137
|
|
|
131
|
-
def
|
|
138
|
+
def disable_events(self) -> 'ConfigurationBuilder':
|
|
139
|
+
"""
|
|
140
|
+
Disable change events for the built :py:class:`~appsettings2.Configuration`.
|
|
141
|
+
|
|
142
|
+
:return: Returns :py:class:`~appsettings2.ConfigurationBuilder` for method chaining.
|
|
143
|
+
"""
|
|
144
|
+
self.__disable_events = True
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
def disableEvents(self) -> 'ConfigurationBuilder':
|
|
148
|
+
"""⚠️ DEPRECATED: use ``disable_events(...)`` instead."""
|
|
149
|
+
return self.disable_events()
|
|
150
|
+
|
|
151
|
+
def enable_watcher(self) -> 'ConfigurationBuilder':
|
|
152
|
+
"""
|
|
153
|
+
Create and assign a :py:class:`~appsettings2.ConfigurationWatcher` instance to the builder.
|
|
154
|
+
|
|
155
|
+
Only creates a new watcher if ``self.__watcher`` is ``None``.
|
|
156
|
+
|
|
157
|
+
:return: Returns :py:class:`~appsettings2.ConfigurationBuilder` for method chaining.
|
|
158
|
+
"""
|
|
159
|
+
if self.__watcher is None:
|
|
160
|
+
from .ConfigurationWatcher import ConfigurationWatcher
|
|
161
|
+
self.__watcher = ConfigurationWatcher()
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def build(self, disable_events: bool | None = None, watcher: Any = None) -> Configuration:
|
|
132
165
|
"""
|
|
133
166
|
Builds a `Configuration` object using the providers which have been added to the builder.
|
|
134
167
|
|
|
168
|
+
:param disable_events: Option indicating whether change events should be emitted, defaults to None (uses value from ``__init__``).
|
|
169
|
+
:param watcher: Optional :py:class:`~appsettings2.ConfigurationWatcher`, defaults to None (uses value from ``__init__``).
|
|
135
170
|
:return: A `Configuration` object, populated with configuration data.
|
|
136
171
|
"""
|
|
172
|
+
effective_watcher = watcher if watcher is not None else self.__watcher
|
|
137
173
|
configuration = Configuration(
|
|
138
|
-
normalize=self.__normalize,
|
|
174
|
+
normalize=self.__normalize,
|
|
175
|
+
scrubkeys=self.__scrubkeys,
|
|
176
|
+
disable_events=disable_events if disable_events is not None else self.__disable_events,
|
|
177
|
+
watcher=effective_watcher)
|
|
139
178
|
for provider in self.__providers:
|
|
140
179
|
provider.populate_configuration(configuration)
|
|
180
|
+
if effective_watcher is not None:
|
|
181
|
+
for provider in self.__providers:
|
|
182
|
+
fp = getattr(provider, 'filepath', None)
|
|
183
|
+
if fp is not None:
|
|
184
|
+
effective_watcher.add_watch(fp)
|
|
141
185
|
return configuration
|
|
142
186
|
|
|
143
187
|
|