dynaconf 3.2.7__tar.gz → 3.2.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. {dynaconf-3.2.7 → dynaconf-3.2.9}/CHANGELOG.md +16 -0
  2. {dynaconf-3.2.7 → dynaconf-3.2.9}/PKG-INFO +1 -1
  3. dynaconf-3.2.9/dynaconf/VERSION +1 -0
  4. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/__init__.py +2 -0
  5. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/base.py +96 -62
  6. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/cli.py +50 -15
  7. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/default_settings.py +1 -0
  8. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/hooking.py +32 -0
  9. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/loaders/__init__.py +36 -9
  10. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/loaders/py_loader.py +9 -1
  11. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/loaders/vault_loader.py +4 -0
  12. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/utils/inspect.py +141 -4
  13. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/utils/parse_conf.py +6 -1
  14. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf.egg-info/PKG-INFO +1 -1
  15. dynaconf-3.2.7/dynaconf/VERSION +0 -1
  16. {dynaconf-3.2.7 → dynaconf-3.2.9}/3.x-release-notes.md +0 -0
  17. {dynaconf-3.2.7 → dynaconf-3.2.9}/CONTRIBUTING.md +0 -0
  18. {dynaconf-3.2.7 → dynaconf-3.2.9}/CONTRIBUTORS.md +0 -0
  19. {dynaconf-3.2.7 → dynaconf-3.2.9}/LICENSE +0 -0
  20. {dynaconf-3.2.7 → dynaconf-3.2.9}/MANIFEST.in +0 -0
  21. {dynaconf-3.2.7 → dynaconf-3.2.9}/README.md +0 -0
  22. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/constants.py +0 -0
  23. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/contrib/__init__.py +0 -0
  24. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/contrib/django_dynaconf_v2.py +0 -0
  25. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/contrib/flask_dynaconf.py +0 -0
  26. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/loaders/base.py +0 -0
  27. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/loaders/env_loader.py +0 -0
  28. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/loaders/ini_loader.py +0 -0
  29. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/loaders/json_loader.py +0 -0
  30. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/loaders/redis_loader.py +0 -0
  31. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/loaders/toml_loader.py +0 -0
  32. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/loaders/yaml_loader.py +0 -0
  33. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/strategies/__init__.py +0 -0
  34. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/strategies/filtering.py +0 -0
  35. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/test_settings.py +0 -0
  36. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/utils/__init__.py +0 -0
  37. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/utils/boxing.py +0 -0
  38. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/utils/files.py +0 -0
  39. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/utils/functional.py +0 -0
  40. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/validator.py +0 -0
  41. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/validator_conditions.py +0 -0
  42. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/__init__.py +0 -0
  43. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/box/__init__.py +0 -0
  44. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/box/box.py +0 -0
  45. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/box/box_list.py +0 -0
  46. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/box/config_box.py +0 -0
  47. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/box/converters.py +0 -0
  48. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/box/exceptions.py +0 -0
  49. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/box/from_file.py +0 -0
  50. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/box/shorthand_box.py +0 -0
  51. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/__init__.py +0 -0
  52. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/_bashcomplete.py +0 -0
  53. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/_compat.py +0 -0
  54. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/_termui_impl.py +0 -0
  55. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/_textwrap.py +0 -0
  56. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/_unicodefun.py +0 -0
  57. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/_winconsole.py +0 -0
  58. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/core.py +0 -0
  59. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/decorators.py +0 -0
  60. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/exceptions.py +0 -0
  61. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/formatting.py +0 -0
  62. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/globals.py +0 -0
  63. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/parser.py +0 -0
  64. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/termui.py +0 -0
  65. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/testing.py +0 -0
  66. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/types.py +0 -0
  67. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/click/utils.py +0 -0
  68. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/dotenv/__init__.py +0 -0
  69. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/dotenv/cli.py +0 -0
  70. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/dotenv/compat.py +0 -0
  71. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/dotenv/ipython.py +0 -0
  72. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/dotenv/main.py +0 -0
  73. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/dotenv/parser.py +0 -0
  74. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/dotenv/version.py +0 -0
  75. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/__init__.py +0 -0
  76. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/__init__.py +0 -0
  77. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/anchor.py +0 -0
  78. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/comments.py +0 -0
  79. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/compat.py +0 -0
  80. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/composer.py +0 -0
  81. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/configobjwalker.py +0 -0
  82. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/constructor.py +0 -0
  83. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/cyaml.py +0 -0
  84. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/dumper.py +0 -0
  85. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/emitter.py +0 -0
  86. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/error.py +0 -0
  87. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/events.py +0 -0
  88. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/loader.py +0 -0
  89. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/main.py +0 -0
  90. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/nodes.py +0 -0
  91. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/parser.py +0 -0
  92. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/reader.py +0 -0
  93. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/representer.py +0 -0
  94. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/resolver.py +0 -0
  95. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/scalarbool.py +0 -0
  96. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/scalarfloat.py +0 -0
  97. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/scalarint.py +0 -0
  98. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/scalarstring.py +0 -0
  99. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/scanner.py +0 -0
  100. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/serializer.py +0 -0
  101. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/setup.py +0 -0
  102. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/timestamp.py +0 -0
  103. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/tokens.py +0 -0
  104. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/ruamel/yaml/util.py +0 -0
  105. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/toml/__init__.py +0 -0
  106. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/toml/decoder.py +0 -0
  107. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/toml/encoder.py +0 -0
  108. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/toml/ordered.py +0 -0
  109. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/toml/tz.py +0 -0
  110. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/tomllib/__init__.py +0 -0
  111. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/tomllib/_parser.py +0 -0
  112. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/tomllib/_re.py +0 -0
  113. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/tomllib/_types.py +0 -0
  114. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf/vendor/tomllib/_writer.py +0 -0
  115. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf.egg-info/SOURCES.txt +0 -0
  116. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf.egg-info/dependency_links.txt +0 -0
  117. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf.egg-info/entry_points.txt +0 -0
  118. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf.egg-info/not-zip-safe +0 -0
  119. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf.egg-info/requires.txt +0 -0
  120. {dynaconf-3.2.7 → dynaconf-3.2.9}/dynaconf.egg-info/top_level.txt +0 -0
  121. {dynaconf-3.2.7 → dynaconf-3.2.9}/setup.cfg +0 -0
  122. {dynaconf-3.2.7 → dynaconf-3.2.9}/setup.py +0 -0
  123. {dynaconf-3.2.7 → dynaconf-3.2.9}/vendor_licenses/box-LICENSE.txt +0 -0
  124. {dynaconf-3.2.7 → dynaconf-3.2.9}/vendor_licenses/click-LICENSE.rst +0 -0
  125. {dynaconf-3.2.7 → dynaconf-3.2.9}/vendor_licenses/licenses.sh +0 -0
  126. {dynaconf-3.2.7 → dynaconf-3.2.9}/vendor_licenses/python-dotenv-LICENSE.txt +0 -0
  127. {dynaconf-3.2.7 → dynaconf-3.2.9}/vendor_licenses/ruamel.yaml-LICENSE.txt +0 -0
  128. {dynaconf-3.2.7 → dynaconf-3.2.9}/vendor_licenses/toml-LICENSE.txt +0 -0
  129. {dynaconf-3.2.7 → dynaconf-3.2.9}/vendor_licenses/tomli-LICENSE.txt +0 -0
  130. {dynaconf-3.2.7 → dynaconf-3.2.9}/vendor_licenses/vendor_versions.txt +0 -0
@@ -2,6 +2,22 @@ Changelog
2
2
  =========
3
3
 
4
4
  <!-- insertion marker -->
5
+ ## [3.2.9](https://github.com/dynaconf/dynaconf/releases/tag/3.2.9) - 2025-02-16
6
+
7
+ ## [3.2.8](https://github.com/dynaconf/dynaconf/releases/tag/3.2.8) - 2025-02-16
8
+
9
+ ### Bug Fixes
10
+
11
+ - Parse data type on merge with comma separated value. *By Bruno Rocha*.
12
+
13
+ ### Features
14
+
15
+ - Add CLI command `debug-info` (#1251). *By Bruno Rocha*.
16
+ - Add support for decorated hooks on settings files (#1246). *By Bruno Rocha*.
17
+ - Add VAULT_TOKEN_RENEW_FOR_DYNACONF config/code (#1094) (#1242). *By Pedro Brochado*.
18
+ - populate_obj takes convert_to_dict (#1237). *By Bruno Rocha*.
19
+ - add VAULT_TOKEN_RENEW. *By Bruno Rocha*.
20
+
5
21
  ## [3.2.7](https://github.com/dynaconf/dynaconf/releases/tag/3.2.7) - 2025-01-21
6
22
 
7
23
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dynaconf
3
- Version: 3.2.7
3
+ Version: 3.2.9
4
4
  Summary: The dynamic configurator for your Python Project
5
5
  Home-page: https://github.com/dynaconf/dynaconf
6
6
  Author: Bruno Rocha
@@ -0,0 +1 @@
1
+ 3.2.9
@@ -4,6 +4,7 @@ from dynaconf.base import LazySettings
4
4
  from dynaconf.constants import DEFAULT_SETTINGS_FILES
5
5
  from dynaconf.contrib import DjangoDynaconf
6
6
  from dynaconf.contrib import FlaskDynaconf
7
+ from dynaconf.hooking import post_hook
7
8
  from dynaconf.utils.inspect import get_history
8
9
  from dynaconf.utils.inspect import inspect_settings
9
10
  from dynaconf.utils.parse_conf import add_converter
@@ -39,4 +40,5 @@ __all__ = [
39
40
  "get_history",
40
41
  "DynaconfFormatError",
41
42
  "DynaconfParseError",
43
+ "post_hook",
42
44
  ]
@@ -1161,6 +1161,10 @@ class Settings:
1161
1161
  """Clean end Execute all loaders"""
1162
1162
  self.clean()
1163
1163
  self._loaded_hooks.clear()
1164
+ for hook in self._post_hooks:
1165
+ with suppress(AttributeError, TypeError):
1166
+ hook._called = False
1167
+
1164
1168
  self.execute_loaders(env, silent)
1165
1169
 
1166
1170
  def execute_loaders(
@@ -1217,7 +1221,13 @@ class Settings:
1217
1221
  last_loader.load(self, env, silent, key)
1218
1222
 
1219
1223
  def load_file(
1220
- self, path=None, env=None, silent=True, key=None, validate=empty
1224
+ self,
1225
+ path=None,
1226
+ env=None,
1227
+ silent=True,
1228
+ key=None,
1229
+ validate=empty,
1230
+ run_hooks=True,
1221
1231
  ):
1222
1232
  """Programmatically load files from ``path``.
1223
1233
 
@@ -1226,82 +1236,95 @@ class Settings:
1226
1236
  - Directory of the last loaded file
1227
1237
  - CWD
1228
1238
 
1229
- :param path: A single filename or a file list
1239
+ :param path: A single filename, a glob or a file list
1230
1240
  :param env: Which env to load from file (default current_env)
1231
1241
  :param silent: Should raise errors?
1232
1242
  :param key: Load a single key?
1233
1243
  :param validate: Should trigger validation?
1244
+ :param run_hooks: Should run collected hooks?
1234
1245
  """
1246
+ files = ensure_a_list(path)
1247
+ if not files: # a glob pattern may return empty
1248
+ return
1249
+
1235
1250
  if validate is empty:
1236
1251
  validate = self.get("VALIDATE_ON_UPDATE_FOR_DYNACONF")
1237
1252
 
1238
1253
  env = (env or self.current_env).upper()
1239
1254
 
1240
- files = ensure_a_list(path)
1241
- if files:
1242
- # Using inspect take the filename and line number of the caller
1243
- # to be used in the source_metadata
1244
- frame = inspect.currentframe()
1245
- caller = inspect.getouterframes(frame)[1]
1246
- already_loaded = set()
1247
- for _filename in files:
1255
+ # Using inspect take the filename and line number of the caller
1256
+ # to be used in the source_metadata
1257
+ frame = inspect.currentframe()
1258
+ caller = inspect.getouterframes(frame)[1]
1259
+
1260
+ already_loaded = set()
1261
+ for _filename in files:
1262
+ # load_file() will handle validation later
1263
+ with suppress(ValidationError):
1264
+ source_metadata = SourceMetadata(
1265
+ loader=f"load_file@{caller.filename}:{caller.lineno}",
1266
+ identifier=_filename,
1267
+ env=env,
1268
+ )
1269
+ if py_loader.try_to_load_from_py_module_name(
1270
+ obj=self,
1271
+ name=_filename,
1272
+ silent=True,
1273
+ identifier=source_metadata,
1274
+ ):
1275
+ # if it was possible to load from module name
1276
+ # continue the loop.
1277
+ continue
1278
+
1279
+ root_dir = str(self._root_path or os.getcwd())
1280
+
1281
+ # Issue #494
1282
+ if (
1283
+ isinstance(_filename, Path)
1284
+ and str(_filename.parent) in root_dir
1285
+ ): # pragma: no cover
1286
+ filepath = str(_filename)
1287
+ else:
1288
+ filepath = os.path.join(root_dir, str(_filename))
1289
+
1290
+ paths = [p for p in sorted(glob(filepath)) if ".local." not in p]
1291
+ local_paths = [p for p in sorted(glob(filepath)) if ".local." in p]
1292
+
1293
+ # Handle possible *.globs sorted alphanumeric
1294
+ for path in paths + local_paths:
1295
+ if path in already_loaded: # pragma: no cover
1296
+ continue
1297
+
1248
1298
  # load_file() will handle validation later
1249
1299
  with suppress(ValidationError):
1250
1300
  source_metadata = SourceMetadata(
1251
1301
  loader=f"load_file@{caller.filename}:{caller.lineno}",
1252
- identifier=_filename,
1302
+ identifier=path,
1253
1303
  env=env,
1254
1304
  )
1255
- if py_loader.try_to_load_from_py_module_name(
1305
+ settings_loader(
1256
1306
  obj=self,
1257
- name=_filename,
1258
- silent=True,
1307
+ env=env,
1308
+ silent=silent,
1309
+ key=key,
1310
+ filename=path,
1311
+ validate=validate,
1259
1312
  identifier=source_metadata,
1260
- ):
1261
- # if it was possible to load from module name
1262
- # continue the loop.
1263
- continue
1264
-
1265
- root_dir = str(self._root_path or os.getcwd())
1266
-
1267
- # Issue #494
1268
- if (
1269
- isinstance(_filename, Path)
1270
- and str(_filename.parent) in root_dir
1271
- ): # pragma: no cover
1272
- filepath = str(_filename)
1273
- else:
1274
- filepath = os.path.join(root_dir, str(_filename))
1275
-
1276
- paths = [
1277
- p for p in sorted(glob(filepath)) if ".local." not in p
1278
- ]
1279
- local_paths = [
1280
- p for p in sorted(glob(filepath)) if ".local." in p
1281
- ]
1282
-
1283
- # Handle possible *.globs sorted alphanumeric
1284
- for path in paths + local_paths:
1285
- if path in already_loaded: # pragma: no cover
1286
- continue
1287
-
1288
- # load_file() will handle validation later
1289
- with suppress(ValidationError):
1290
- source_metadata = SourceMetadata(
1291
- loader=f"load_file@{caller.filename}:{caller.lineno}",
1292
- identifier=path,
1293
- env=env,
1294
- )
1295
- settings_loader(
1296
- obj=self,
1297
- env=env,
1298
- silent=silent,
1299
- key=key,
1300
- filename=path,
1301
- validate=validate,
1302
- identifier=source_metadata,
1303
- )
1304
- already_loaded.add(path)
1313
+ )
1314
+ already_loaded.add(path)
1315
+
1316
+ if run_hooks:
1317
+ # this will call any collected hook that was not called yet
1318
+ execute_instance_hooks(
1319
+ self,
1320
+ "post",
1321
+ [
1322
+ _hook
1323
+ for _hook in self._post_hooks
1324
+ if getattr(_hook, "_dynaconf_hook", False) is True
1325
+ and not getattr(_hook, "_called", False)
1326
+ ],
1327
+ )
1305
1328
 
1306
1329
  # handle param `validate`
1307
1330
  if validate is True:
@@ -1385,22 +1408,33 @@ class Settings:
1385
1408
  value = self.get_fresh(key)
1386
1409
  return value is True or value in true_values
1387
1410
 
1388
- def populate_obj(self, obj, keys=None, ignore=None, internal=False):
1411
+ def populate_obj(
1412
+ self,
1413
+ obj,
1414
+ keys=None,
1415
+ ignore=None,
1416
+ internal=False,
1417
+ convert_to_dict=False,
1418
+ ):
1389
1419
  """Given the `obj` populate it using self.store items.
1390
1420
 
1391
1421
  :param obj: An object to be populated, a class instance.
1392
1422
  :param keys: A list of keys to be included.
1393
1423
  :param ignore: A list of keys to be excluded.
1424
+ :param internal: Include internal keys.
1425
+ :param convert_to_dict: Convert the settings to a pure dict (no Box)
1426
+ before populating.
1394
1427
  """
1428
+ data = self.to_dict(internal=internal) if convert_to_dict else self
1395
1429
  keys = keys or self.keys()
1396
1430
  for key in keys:
1431
+ key = upperfy(key)
1397
1432
  if not internal:
1398
1433
  if key in UPPER_DEFAULT_SETTINGS:
1399
1434
  continue
1400
- key = upperfy(key)
1401
1435
  if ignore and key in ignore:
1402
1436
  continue
1403
- value = self.get(key, empty)
1437
+ value = data.get(key, empty)
1404
1438
  if value is not empty:
1405
1439
  setattr(obj, key, value)
1406
1440
 
@@ -28,6 +28,7 @@ from dynaconf.utils.inspect import EnvNotFoundError
28
28
  from dynaconf.utils.inspect import inspect_settings
29
29
  from dynaconf.utils.inspect import KeyNotFoundError
30
30
  from dynaconf.utils.inspect import OutputFormatError
31
+ from dynaconf.utils.inspect import print_debug_info
31
32
  from dynaconf.utils.parse_conf import parse_conf_data
32
33
  from dynaconf.utils.parse_conf import unparse_conf_data
33
34
  from dynaconf.validator import ValidationError
@@ -54,7 +55,12 @@ def set_settings(ctx, instance=None):
54
55
 
55
56
  settings = None
56
57
 
57
- _echo_enabled = ctx.invoked_subcommand not in ["get", "inspect", None]
58
+ _echo_enabled = ctx.invoked_subcommand not in [
59
+ "get",
60
+ "inspect",
61
+ "debug-info",
62
+ None,
63
+ ]
58
64
  if "--json" in click.get_os_args():
59
65
  _echo_enabled = False
60
66
 
@@ -857,7 +863,10 @@ INSPECT_FORMATS = list(builtin_dumpers.keys())
857
863
  @main.command()
858
864
  @click.option("--key", "-k", help="Filters result by key.")
859
865
  @click.option(
860
- "--env", "-e", help="Filters result by environment.", default=None
866
+ "--env",
867
+ "-e",
868
+ help="Filters result by environment on --report-mode=inspect.",
869
+ default=None,
861
870
  )
862
871
  @click.option(
863
872
  "--format",
@@ -870,7 +879,7 @@ INSPECT_FORMATS = list(builtin_dumpers.keys())
870
879
  "--old-first",
871
880
  "new_first",
872
881
  "-s",
873
- help="Invert history sorting to 'old-first'",
882
+ help="Invert history sorting to 'old-first' on --report-mode=inspect.",
874
883
  default=True,
875
884
  is_flag=True,
876
885
  )
@@ -880,7 +889,7 @@ INSPECT_FORMATS = list(builtin_dumpers.keys())
880
889
  "-n",
881
890
  default=None,
882
891
  type=int,
883
- help="Limits how many history entries are shown.",
892
+ help="Limits how many history entries are shown on --report-mode=inspect.",
884
893
  )
885
894
  @click.option(
886
895
  "--all",
@@ -890,28 +899,54 @@ INSPECT_FORMATS = list(builtin_dumpers.keys())
890
899
  is_flag=True,
891
900
  help="Show dynaconf internal settings?",
892
901
  )
902
+ @click.option(
903
+ "--report-mode",
904
+ "report_mode",
905
+ "-m",
906
+ default="inspect",
907
+ type=click.Choice(["inspect", "debug"]),
908
+ )
909
+ @click.option(
910
+ "-v",
911
+ "--verbose",
912
+ count=True,
913
+ help="Increase verbosity of the output on --report-mode=debug.",
914
+ )
893
915
  def inspect(
894
- key, env, format, new_first, history_limit, _all
916
+ key, env, format, new_first, history_limit, _all, report_mode, verbose
895
917
  ): # pragma: no cover
896
918
  """
897
919
  Inspect the loading history of the given settings instance.
898
920
 
899
921
  Filters by key and environment, otherwise shows all.
900
922
  """
901
- try:
902
- inspect_settings(
923
+
924
+ if report_mode == "debug":
925
+ print_debug_info(
903
926
  settings,
904
- key=key,
905
- env=env or None,
906
927
  dumper=format,
907
- new_first=new_first,
908
- include_internal=_all,
909
- history_limit=history_limit,
910
- print_report=True,
928
+ verbosity=verbose,
929
+ key=key,
911
930
  )
912
931
  click.echo()
913
- except (KeyNotFoundError, EnvNotFoundError, OutputFormatError) as err:
914
- click.echo(err)
932
+ elif report_mode == "inspect":
933
+ try:
934
+ inspect_settings(
935
+ settings,
936
+ key=key,
937
+ env=env or None,
938
+ dumper=format,
939
+ new_first=new_first,
940
+ include_internal=_all,
941
+ history_limit=history_limit,
942
+ print_report=True,
943
+ )
944
+ click.echo()
945
+ except (KeyNotFoundError, EnvNotFoundError, OutputFormatError) as err:
946
+ click.echo(err)
947
+ sys.exit(1)
948
+ else:
949
+ click.echo("Invalid report mode")
915
950
  sys.exit(1)
916
951
 
917
952
 
@@ -191,6 +191,7 @@ VAULT_ROLE_ID_FOR_DYNACONF = get("VAULT_ROLE_ID_FOR_DYNACONF", None)
191
191
  VAULT_SECRET_ID_FOR_DYNACONF = get("VAULT_SECRET_ID_FOR_DYNACONF", None)
192
192
  VAULT_USERNAME_FOR_DYNACONF = get("VAULT_USERNAME_FOR_DYNACONF", None)
193
193
  VAULT_PASSWORD_FOR_DYNACONF = get("VAULT_PASSWORD_FOR_DYNACONF", None)
194
+ VAULT_TOKEN_RENEW_FOR_DYNACONF = get("VAULT_TOKEN_RENEW_FOR_DYNACONF", False)
194
195
 
195
196
  # Only core loaders defined on this list will be invoked
196
197
  core_loaders = ["YAML", "TOML", "INI", "JSON", "PY"]
@@ -18,6 +18,7 @@ __all__ = [
18
18
  "MethodValue",
19
19
  "Action",
20
20
  "HookableSettings",
21
+ "post_hook",
21
22
  ]
22
23
 
23
24
 
@@ -341,3 +342,34 @@ class TempSettingsHolder:
341
342
  else:
342
343
  self._initialize()
343
344
  setattr(self._settings, attr, value)
345
+
346
+
347
+ def post_hook(function: Callable) -> Callable:
348
+ """This decorator marks a function as a post hook.
349
+ This works by adding the _dynaconf_hook attribute to the function,
350
+ then the python loader, when reading the module, will look for
351
+ this attribute and register the function as a post_hook for the settings.
352
+
353
+ e.g: On a settings file with .py extension:
354
+
355
+ from dynaconf import post_hook
356
+ @post_hook
357
+ def set_log_handlers(settings) -> dict:
358
+ data = {} # data to be merged into settings
359
+
360
+ # conditionals
361
+ if (logging := settings.get('LOGGING')) is not None:
362
+ # do something with logging
363
+ # add it back to data
364
+ data['LOGGING'] = logging
365
+ return data
366
+ """
367
+ try:
368
+ function._dynaconf_hook = True # type: ignore
369
+ function._called = False # type: ignore
370
+ function._dynaconf_hook_source = function.__module__ # type: ignore
371
+ except (AttributeError, TypeError):
372
+ raise TypeError(
373
+ "post_hook decorator must be applied to a function or method."
374
+ )
375
+ return function
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import importlib
4
4
  import os
5
+ from contextlib import suppress
5
6
  from typing import Callable
6
7
  from typing import TYPE_CHECKING
7
8
 
@@ -146,6 +147,22 @@ def execute_module_hooks(
146
147
  execute_hooks = execute_module_hooks
147
148
 
148
149
 
150
+ def _get_unique_hook_id(hook_func, hook_source):
151
+ """get unique identifier for a hook function.
152
+ in most of cases this will be the function name@source_file
153
+ however, if the function is a lambda, it will be a hash of the code object.
154
+ because lambda functions are not hashable itself and we can't rely on its id.
155
+ """
156
+ hook_unique_id = hook_func.__name__
157
+ if hook_unique_id == "<lambda>":
158
+ frame_info = getattr(hook_func, "__code__", None)
159
+ if frame_info:
160
+ hook_unique_id = f"lambda_{hash(frame_info.co_code)}"
161
+ else:
162
+ hook_unique_id = f"lambda_{id(hook_func)}"
163
+ return f"{hook_unique_id}@{hook_source}"
164
+
165
+
149
166
  def _run_hook_module(hook_type, hook_module, obj, key=""):
150
167
  """
151
168
  Run a hook function from hook_module.
@@ -153,11 +170,6 @@ def _run_hook_module(hook_type, hook_module, obj, key=""):
153
170
  Given a @hook_type, a @hook_module and a settings @obj, load the function
154
171
  and execute it if found.
155
172
  """
156
- hook_source = hook_module.__file__
157
-
158
- # check if already loaded
159
- if hook_type in obj._loaded_hooks.get(hook_source, {}):
160
- return
161
173
 
162
174
  # check errors
163
175
  if hook_module and getattr(hook_module, "_error", False):
@@ -165,9 +177,12 @@ def _run_hook_module(hook_type, hook_module, obj, key=""):
165
177
  raise hook_module._error
166
178
 
167
179
  # execute hook
180
+ hook_source = hook_module.__file__
168
181
  hook_func = getattr(hook_module, hook_type, None)
169
182
  if hook_func:
170
- _run_hook_function(obj, hook_type, hook_func, hook_source, key)
183
+ identifier = _get_unique_hook_id(hook_func, hook_source)
184
+ if hook_type not in obj._loaded_hooks.get(identifier, {}):
185
+ _run_hook_function(obj, hook_type, hook_func, hook_source, key)
171
186
 
172
187
 
173
188
  def _run_hook_function(
@@ -183,15 +198,27 @@ def _run_hook_function(
183
198
  It execute @hook_func, update the results into settings @obj and
184
199
  add it to _loaded_hook registry ([@hook_source][@hook_type])
185
200
  """
201
+ # if the function has a _dynaconf_hook_source attribute set
202
+ # hook_source to it
203
+ hook_source = getattr(hook_func, "_dynaconf_hook_source", hook_source)
204
+
186
205
  # optional settings argument
187
206
  try:
188
207
  hook_dict = hook_func(obj.dynaconf.clone())
189
208
  except TypeError:
190
209
  hook_dict = hook_func()
191
210
 
192
- # update obj settings
211
+ # mark as called so executors such as `load_file` can avoid calling it again
212
+ with suppress(AttributeError, TypeError):
213
+ # callable may not be writable, the caveat is that it will be called again in case of reload
214
+ # however, this must not be a problem since the function should be idempotent
215
+ # and documentation warns about this behavior.
216
+ hook_func._called = True
217
+
218
+ identifier = _get_unique_hook_id(hook_func, hook_source)
219
+
193
220
  if hook_dict:
194
- identifier = f"{hook_func.__name__}@{hook_source}"
221
+ # update obj settings
195
222
  merge = hook_dict.pop(
196
223
  "dynaconf_merge", hook_dict.pop("DYNACONF_MERGE", False)
197
224
  )
@@ -212,7 +239,7 @@ def _run_hook_function(
212
239
  )
213
240
 
214
241
  # add to registry
215
- obj._loaded_hooks[hook_source][hook_type] = hook_dict
242
+ obj._loaded_hooks[identifier][hook_type] = hook_dict
216
243
 
217
244
 
218
245
  def settings_loader(
@@ -57,13 +57,13 @@ def load_from_python_object(
57
57
  file_merge = getattr(mod, "DYNACONF_MERGE", empty)
58
58
 
59
59
  for setting in dir(mod):
60
+ setting_value = getattr(mod, setting)
60
61
  # A setting var in a Python file should start with upper case
61
62
  # valid: A_value=1, ABC_value=3 A_BBB__default=1
62
63
  # invalid: a_value=1, MyValue=3
63
64
  # This is to avoid loading functions, classes and built-ins
64
65
  if setting.split("__")[0].isupper():
65
66
  if key is None or key == setting:
66
- setting_value = getattr(mod, setting)
67
67
  obj.set(
68
68
  setting,
69
69
  setting_value,
@@ -71,6 +71,14 @@ def load_from_python_object(
71
71
  merge=file_merge,
72
72
  validate=validate,
73
73
  )
74
+ # if setting is a post_hook function it will be a callable with
75
+ # the _dynaconf_hook attribute set to True
76
+ # then we want to add it to the post_hooks list on the obj.
77
+ elif callable(setting_value) and getattr(
78
+ setting_value, "_dynaconf_hook", False
79
+ ):
80
+ if setting_value not in obj._post_hooks:
81
+ obj._post_hooks.append(setting_value)
74
82
 
75
83
  obj._loaded_py_modules.append(mod.__name__)
76
84
  obj._loaded_files.append(mod.__file__)
@@ -62,6 +62,10 @@ def get_client(obj):
62
62
  credentials.token,
63
63
  role=obj.VAULT_AUTH_ROLE_FOR_DYNACONF,
64
64
  )
65
+
66
+ if obj.VAULT_TOKEN_RENEW_FOR_DYNACONF:
67
+ client.auth.token.renew_self()
68
+
65
69
  assert client.is_authenticated(), (
66
70
  "Vault authentication error: is VAULT_TOKEN_FOR_DYNACONF or "
67
71
  "VAULT_ROLE_ID_FOR_DYNACONF defined?"
@@ -4,7 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import sys
7
+ from contextlib import suppress
7
8
  from functools import partial
9
+ from importlib.metadata import PackageNotFoundError
10
+ from importlib.metadata import version
8
11
  from pathlib import PosixPath
9
12
  from typing import Any
10
13
  from typing import Callable
@@ -28,11 +31,21 @@ if TYPE_CHECKING: # pragma: no cover
28
31
 
29
32
  # Dumpers config
30
33
 
31
- json_pretty = partial(json.dump, indent=2)
32
- json_compact = json.dump
34
+ json_pretty = partial(json.dump, indent=2, default=str)
35
+ json_compact = partial(json.dump, default=str)
36
+
37
+
38
+ def yaml_dumper_with_defaults(data: dict, text_stream: TextIO) -> None:
39
+ """Easier way to get YAML dumper to handle unseralizable types"""
40
+ yaml = YAML()
41
+ # let JSON handle unserializable types and then load it back
42
+ data = json.loads(json.dumps(data, default=str))
43
+ yaml.default_flow_style = False
44
+ yaml.dump(data, text_stream)
45
+
33
46
 
34
47
  builtin_dumpers = {
35
- "yaml": YAML().dump,
48
+ "yaml": yaml_dumper_with_defaults,
36
49
  "json": json_pretty,
37
50
  "json-compact": json_compact,
38
51
  }
@@ -312,7 +325,7 @@ def _get_data_by_key(
312
325
  key_dotted_path: str,
313
326
  default: Any = None,
314
327
  sep="__",
315
- ):
328
+ ) -> Any:
316
329
  """
317
330
  Returns value found in data[key] using dot-path str (e.g, "path.to.key").
318
331
  Raises KeyError if not found
@@ -344,3 +357,127 @@ def _get_data_by_key(
344
357
  if not default:
345
358
  raise KeyError(f"Path not found in data: {key_dotted_path!r}")
346
359
  return default
360
+
361
+
362
+ def get_debug_info(
363
+ settings: Settings | LazySettings,
364
+ verbosity: int = 0,
365
+ key: str | None = None,
366
+ ) -> dict:
367
+ """Returns a dict with debug info about the settings object"""
368
+
369
+ if key:
370
+ verbosity = 2
371
+
372
+ def filter_by_key(data: dict) -> dict:
373
+ """If key is not None, filter dict keeping only the key"""
374
+ if key:
375
+ try:
376
+ return {key: _get_data_by_key(data, key)}
377
+ except KeyError:
378
+ return {}
379
+ return data
380
+
381
+ def build_loading_history() -> list[dict]:
382
+ _data = []
383
+ for (
384
+ source_metadata,
385
+ source_data,
386
+ ) in settings._loaded_by_loaders.items():
387
+ _real_data = filter_by_key(source_data)
388
+ if verbosity == 0:
389
+ _data.append(
390
+ {
391
+ "loader": source_metadata.loader,
392
+ "identifier": source_metadata.identifier,
393
+ "data": len(_real_data),
394
+ }
395
+ )
396
+ elif verbosity == 1:
397
+ _data.append(
398
+ {
399
+ "loader": source_metadata.loader,
400
+ "identifier": source_metadata.identifier,
401
+ "data": list(_real_data.keys()),
402
+ }
403
+ )
404
+ else:
405
+ _data.append(
406
+ {
407
+ "loader": source_metadata.loader,
408
+ "identifier": source_metadata.identifier,
409
+ "data": _real_data,
410
+ }
411
+ )
412
+ return _data
413
+
414
+ def build_loaded_hooks():
415
+ _data = []
416
+ for hook, hook_data in settings._loaded_hooks.items():
417
+ _real_data = filter_by_key(hook_data.get("post", {}))
418
+ if verbosity == 0:
419
+ _data.append(
420
+ {
421
+ "hook": str(hook),
422
+ "data": len(_real_data),
423
+ }
424
+ )
425
+ elif verbosity == 1:
426
+ _data.append(
427
+ {
428
+ "hook": str(hook),
429
+ "data": list(_real_data.keys()),
430
+ }
431
+ )
432
+ else:
433
+ _data.append(
434
+ {
435
+ "hook": str(hook),
436
+ "data": _real_data,
437
+ }
438
+ )
439
+ return _data
440
+
441
+ data = {
442
+ "versions": {
443
+ "dynaconf": version("dynaconf"),
444
+ },
445
+ "root_path": settings._root_path,
446
+ "validators": [str(v) for v in settings.validators],
447
+ "core_loaders": settings._loaders,
448
+ "loaded_files": settings._loaded_files,
449
+ "history": build_loading_history(),
450
+ "post_hooks": [str(h) for h in settings._post_hooks],
451
+ "loaded_hooks": build_loaded_hooks(),
452
+ }
453
+ for name in ["django", "flask", "fastapi", "starlette"]:
454
+ with suppress(PackageNotFoundError):
455
+ data["versions"][name] = version(name)
456
+
457
+ if settings.get("ENVIRONMENTS_FOR_DYNACONF"):
458
+ data["environments"] = list(settings.get("ENVIRONMENTS_FOR_DYNACONF"))
459
+ data["loaded_envs"] = settings._loaded_envs
460
+
461
+ if key:
462
+ data["current"] = {key: settings.get(key)}
463
+ return data
464
+
465
+
466
+ def print_debug_info(
467
+ settings: Settings | LazySettings,
468
+ *,
469
+ dumper: DumperPreset | DumperType | None = None,
470
+ verbosity: int = 0,
471
+ key: str | None = None,
472
+ ):
473
+ """Calls dumper with settings debug info"""
474
+ dumper = dumper or "yaml"
475
+ if isinstance(dumper, str):
476
+ dumper = builtin_dumpers.get(dumper)
477
+ if dumper is None:
478
+ raise OutputFormatError(
479
+ f"The desired format is not available: {dumper!r}"
480
+ )
481
+
482
+ data = get_debug_info(settings, verbosity, key)
483
+ dumper(data, sys.stdout)
@@ -133,7 +133,12 @@ class Merge(MetaValue):
133
133
  }
134
134
  elif "," in self.value:
135
135
  # @merge foo,bar
136
- self.value = self.value.split(",")
136
+ self.value = [
137
+ parse_conf_data(
138
+ v, tomlfy=True, box_settings=box_settings
139
+ )
140
+ for v in self.value.split(",")
141
+ ]
137
142
  else:
138
143
  # @merge foo
139
144
  self.value = [self.value]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dynaconf
3
- Version: 3.2.7
3
+ Version: 3.2.9
4
4
  Summary: The dynamic configurator for your Python Project
5
5
  Home-page: https://github.com/dynaconf/dynaconf
6
6
  Author: Bruno Rocha
@@ -1 +0,0 @@
1
- 3.2.7
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes