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.
Files changed (25) hide show
  1. {appsettings2-2.0.10/src/appsettings2.egg-info → appsettings2-2.1.0}/PKG-INFO +4 -25
  2. {appsettings2-2.0.10 → appsettings2-2.1.0}/pyproject.toml +7 -7
  3. {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/Configuration.py +160 -37
  4. {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/ConfigurationBuilder.py +48 -4
  5. appsettings2-2.1.0/src/appsettings2/ConfigurationWatcher.py +375 -0
  6. appsettings2-2.1.0/src/appsettings2/SKILL.md +475 -0
  7. {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/__init__.py +7 -3
  8. {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/helpers.py +4 -2
  9. {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/providers/ConfigurationProvider.py +2 -1
  10. {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/providers/JsonConfigurationProvider.py +12 -4
  11. {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/providers/TomlConfigurationProvider.py +13 -5
  12. {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/providers/YamlConfigurationProvider.py +14 -6
  13. {appsettings2-2.0.10 → appsettings2-2.1.0/src/appsettings2.egg-info}/PKG-INFO +4 -25
  14. {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2.egg-info/SOURCES.txt +2 -0
  15. {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2.egg-info/requires.txt +2 -2
  16. {appsettings2-2.0.10 → appsettings2-2.1.0}/LICENSE +0 -0
  17. {appsettings2-2.0.10 → appsettings2-2.1.0}/README.md +0 -0
  18. {appsettings2-2.0.10 → appsettings2-2.1.0}/setup.cfg +0 -0
  19. {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/ConfigurationException.py +0 -0
  20. {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/providers/CommandLineConfigurationProvider.py +1 -1
  21. {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/providers/EnvironmentConfigurationProvider.py +0 -0
  22. {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/providers/__init__.py +0 -0
  23. {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2/py.typed +0 -0
  24. {appsettings2-2.0.10 → appsettings2-2.1.0}/src/appsettings2.egg-info/dependency_links.txt +0 -0
  25. {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.10
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 License
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.2
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.10.8; extra == "dev"
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.10"
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 = {file = "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.2"
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.10.8)",
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-ignore = [
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
- def __init__(self, normalize: bool = False, scrubkeys: bool = False) -> None:
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
- while len(self.__keys) > 0:
405
- t = self.__keys.popitem()
406
- delattr(self, t[1])
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) -> 'Configuration':
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 build(self) -> Configuration:
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, scrubkeys=self.__scrubkeys)
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