dotenvmodel 0.6.1__tar.gz → 0.6.3__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 (118) hide show
  1. dotenvmodel-0.6.3/.release-please-manifest.json +3 -0
  2. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/CHANGELOG.md +14 -0
  3. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/PKG-INFO +1 -1
  4. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/guides/loading.md +2 -2
  5. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/__init__.py +1 -1
  6. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/caching.py +18 -5
  7. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/config.py +23 -5
  8. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/pyproject.toml +1 -1
  9. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_cached.py +128 -2
  10. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/uv.lock +1 -1
  11. dotenvmodel-0.6.1/.release-please-manifest.json +0 -3
  12. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/.github/CODEOWNERS +0 -0
  13. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  14. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  15. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/.github/dependabot.yml +0 -0
  16. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/.github/pull_request_template.md +0 -0
  17. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/.github/workflows/ci.yml +0 -0
  18. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/.github/workflows/docs.yml +0 -0
  19. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/.github/workflows/publish.yml +0 -0
  20. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/.github/workflows/release-please.yml +0 -0
  21. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/.gitignore +0 -0
  22. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/.python-version +0 -0
  23. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/CODE_OF_CONDUCT.md +0 -0
  24. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/CONTRIBUTING.md +0 -0
  25. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/LICENSE +0 -0
  26. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/Makefile +0 -0
  27. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/README.md +0 -0
  28. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/SECURITY.md +0 -0
  29. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/api-reference/caching.md +0 -0
  30. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/api-reference/coercion.md +0 -0
  31. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/api-reference/config.md +0 -0
  32. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/api-reference/constants.md +0 -0
  33. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/api-reference/describe-formatters.md +0 -0
  34. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/api-reference/describe-renderers.md +0 -0
  35. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/api-reference/describe.md +0 -0
  36. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/api-reference/dotenvmodel.md +0 -0
  37. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/api-reference/exceptions.md +0 -0
  38. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/api-reference/fields.md +0 -0
  39. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/api-reference/loading.md +0 -0
  40. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/api-reference/logging-config.md +0 -0
  41. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/api-reference/metaclass.md +0 -0
  42. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/api-reference/types.md +0 -0
  43. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/api-reference/validation.md +0 -0
  44. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/changelog.md +0 -0
  45. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/examples/complete-app.md +0 -0
  46. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/getting-started/installation.md +0 -0
  47. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/getting-started/quick-start.md +0 -0
  48. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/guides/configuration-docs.md +0 -0
  49. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/guides/error-handling.md +0 -0
  50. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/guides/fields.md +0 -0
  51. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/guides/logging.md +0 -0
  52. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/guides/prefixes.md +0 -0
  53. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/guides/types.md +0 -0
  54. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/guides/validation.md +0 -0
  55. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/docs/index.md +0 -0
  56. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/_constants.py +0 -0
  57. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/_redaction.py +0 -0
  58. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/coercion.py +0 -0
  59. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/describe/__init__.py +0 -0
  60. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/describe/formatters.py +0 -0
  61. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/describe/renderers.py +0 -0
  62. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/exceptions.py +0 -0
  63. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/fields.py +0 -0
  64. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/loading.py +0 -0
  65. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/logging_config.py +0 -0
  66. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/metaclass.py +0 -0
  67. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/py.typed +0 -0
  68. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/types.py +0 -0
  69. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/dotenvmodel/validation.py +0 -0
  70. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/examples/advanced_types.py +0 -0
  71. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/examples/basic_usage.py +0 -0
  72. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/examples/describe_documentation.py +0 -0
  73. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/examples/generate_env_example.py +0 -0
  74. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/examples/logging_example.py +0 -0
  75. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/mkdocs.yml +0 -0
  76. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/release-please-config.json +0 -0
  77. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/__init__.py +0 -0
  78. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/conftest.py +0 -0
  79. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_basic.py +0 -0
  80. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_coercion.py +0 -0
  81. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_collection_validators.py +0 -0
  82. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_defaults_coercion.py +0 -0
  83. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_describe.py +0 -0
  84. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_edge_adoption.py +0 -0
  85. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_edge_datetime.py +0 -0
  86. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_edge_describe_output.py +0 -0
  87. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_edge_enum.py +0 -0
  88. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_edge_field_options.py +0 -0
  89. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_edge_numbers.py +0 -0
  90. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_edge_path_options.py +0 -0
  91. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_edge_strings.py +0 -0
  92. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_empty_collection_items.py +0 -0
  93. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_empty_strings.py +0 -0
  94. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_enum.py +0 -0
  95. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_errors.py +0 -0
  96. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_field_constraints.py +0 -0
  97. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_future_annotations.py +0 -0
  98. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_inheritance.py +0 -0
  99. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_json.py +0 -0
  100. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_json_and_optional.py +0 -0
  101. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_loading.py +0 -0
  102. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_logging_config.py +0 -0
  103. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_nested_config.py +0 -0
  104. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_post_load.py +0 -0
  105. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_prefix.py +0 -0
  106. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_reload.py +0 -0
  107. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_secret_redaction.py +0 -0
  108. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_secretstr_security.py +0 -0
  109. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_string_affixes.py +0 -0
  110. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_strip.py +0 -0
  111. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_strip_integration.py +0 -0
  112. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_types.py +0 -0
  113. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_union_types.py +0 -0
  114. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_url_dsn.py +0 -0
  115. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_url_password_decoding.py +0 -0
  116. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_url_unquote.py +0 -0
  117. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_urls.py +0 -0
  118. {dotenvmodel-0.6.1 → dotenvmodel-0.6.3}/tests/test_validator.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.6.3"
3
+ }
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.3](https://github.com/AZX-PBC-OSS/dotenvmodel/compare/v0.6.2...v0.6.3) (2026-07-28)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * reload() must update the instance's recorded load arguments ([#56](https://github.com/AZX-PBC-OSS/dotenvmodel/issues/56)) ([a293461](https://github.com/AZX-PBC-OSS/dotenvmodel/commit/a2934612da52130cc539be65bd1fe196a4381dde))
14
+
15
+ ## [0.6.2](https://github.com/AZX-PBC-OSS/dotenvmodel/compare/v0.6.1...v0.6.2) (2026-07-28)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * warn on cached() argument disagreement, not on non-default args ([#54](https://github.com/AZX-PBC-OSS/dotenvmodel/issues/54)) ([ba80777](https://github.com/AZX-PBC-OSS/dotenvmodel/commit/ba80777e7486797d8a33d27b124b069f90664de6))
21
+
8
22
  ## [0.6.1](https://github.com/AZX-PBC-OSS/dotenvmodel/compare/v0.6.0...v0.6.1) (2026-07-27)
9
23
 
10
24
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotenvmodel
3
- Version: 0.6.1
3
+ Version: 0.6.3
4
4
  Summary: Type-safe environment configuration with automatic .env file loading
5
5
  Project-URL: Homepage, https://github.com/AZX-PBC-OSS/dotenvmodel
6
6
  Project-URL: Repository, https://github.com/AZX-PBC-OSS/dotenvmodel
@@ -222,9 +222,9 @@ assert config is same_config
222
222
 
223
223
  `cached()` is lazy — the environment is only read on the very first call. It is also thread-safe: if multiple threads call `cached()` simultaneously before the first load completes, they race on a lock and only one thread calls `load()`; the rest block and receive the same instance. Once the cache is warm, all calls return immediately without acquiring the lock.
224
224
 
225
- Arguments (`env`, `override`, `env_dir`) are only used on the first call. Once the instance is cached, subsequent calls ignore any arguments and return the existing instance. A warning is logged if non-default arguments are passed against an already-warm cache.
225
+ Arguments (`env`, `override`, `env_dir`) are only used on the first call. Once the instance is cached, subsequent calls ignore any arguments and return the existing instance. A warning is logged when the arguments passed disagree with the ones the cached instance was loaded with — so an accessor that consistently passes the same non-default arguments (`override=False`, say) stays silent, while a caller asking for something the cache does not hold is told what it actually holds.
226
226
 
227
- Calling `.reload()` on the cached instance mutates it in place; since `cached()` always returns the same object, subsequent `cached()` calls see the reloaded values.
227
+ Calling `.reload()` on the cached instance mutates it in place; since `cached()` always returns the same object, subsequent `cached()` calls see the reloaded values. A reload also updates what the instance reports as its load arguments, so `reload(env="prod")` is reflected by both `loaded_with()` and the comparison above.
228
228
 
229
229
  !!! warning "Reentrant `cached()` calls raise `RuntimeError`"
230
230
 
@@ -37,7 +37,7 @@ Public API:
37
37
  Exception hierarchy
38
38
  """
39
39
 
40
- __version__ = "0.6.1" # x-release-please-version
40
+ __version__ = "0.6.3" # x-release-please-version
41
41
  __author__ = "AZX, PBC."
42
42
  __email__ = "oss@azx.io"
43
43
  __license__ = "MIT"
@@ -141,8 +141,8 @@ def acquire_cached(
141
141
 
142
142
  Implements double-checked locking: a lock-free fast path checks
143
143
  ``cls.__dict__``; if the cache is warm, the existing instance is returned
144
- immediately (with a warning if non-default arguments were passed against
145
- an already-warm cache). If the cache is cold, a module-level lock is
144
+ immediately (with a warning if arguments that disagree with the ones
145
+ that populated the cache are passed). If the cache is cold, a module-level lock is
146
146
  acquired and the check is repeated before calling ``cls.load()``.
147
147
 
148
148
  Reentrant calls for the same class from within that class's own
@@ -168,15 +168,28 @@ def acquire_cached(
168
168
  """
169
169
  cached = get_cached(cls)
170
170
  if cached is not None:
171
- if env is not None or override is not True or env_dir is not None:
171
+ # Warn on DISAGREEMENT, not on non-default arguments. An application accessor that
172
+ # consistently passes the same non-default arguments (e.g. `override=False`) on every
173
+ # call is asking for exactly what it already got, and warning it every time would train
174
+ # readers to filter the message out. A caller passing something the cache was NOT built
175
+ # with is the real bug this catches, and it still fires.
176
+ #
177
+ # Compared against the INSTANCE's own record of how it was loaded — the same fields
178
+ # `reload()` reuses — rather than a second copy kept beside the cache. One source of
179
+ # truth, and self-correcting: `reload(env="prod")` updates them, so a later `cached()`
180
+ # is judged against what the cached object actually holds now, not against whatever
181
+ # populated it originally.
182
+ loaded = cached.loaded_with()
183
+ if (env, override, env_dir) != loaded:
172
184
  logger.warning(
173
185
  "cached() called on %s with arguments (env=%r, override=%r, "
174
- "env_dir=%r) but the cache is already populated; "
175
- "arguments were ignored.",
186
+ "env_dir=%r) but the cache is already populated with "
187
+ "(env=%r, override=%r, env_dir=%r); arguments were ignored.",
176
188
  cls.__name__,
177
189
  env,
178
190
  override,
179
191
  env_dir,
192
+ *loaded,
180
193
  )
181
194
  return cached
182
195
 
@@ -567,6 +567,20 @@ class DotEnvConfig(metaclass=ConfigMeta):
567
567
  instance._load_env_dir = env_dir
568
568
  return instance
569
569
 
570
+ def loaded_with(self) -> tuple[str | None, bool, Path | None]:
571
+ """The ``(env, override, env_dir)`` this instance was last loaded with.
572
+
573
+ `reload()` uses it to repeat a load without restating its arguments — so a SIGHUP
574
+ handler calling `reload()` with no arguments keeps the original precedence rather than
575
+ silently reverting to `override=True`. `cached()`'s warm path uses it to tell a caller
576
+ who agrees with how the cache was built from one who disagrees.
577
+
578
+ Exposed rather than read field-by-field so there is one definition of "how was this
579
+ loaded", and callers outside this class do not reach into three private attributes.
580
+ Values reflect the most recent `reload()`, not only the original `load()`.
581
+ """
582
+ return (self._load_env, self._load_override, self._load_env_dir)
583
+
570
584
  def reload(
571
585
  self,
572
586
  env: str | None = None,
@@ -628,9 +642,10 @@ class DotEnvConfig(metaclass=ConfigMeta):
628
642
  """
629
643
  logger.info(f"Reloading {self.__class__.__name__} configuration")
630
644
 
631
- reload_env = env if env is not None else self._load_env
632
- reload_override = override if override is not None else self._load_override
633
- reload_env_dir = env_dir if env_dir is not None else self._load_env_dir
645
+ loaded_env, loaded_override, loaded_env_dir = self.loaded_with()
646
+ reload_env = env if env is not None else loaded_env
647
+ reload_override = override if override is not None else loaded_override
648
+ reload_env_dir = env_dir if env_dir is not None else loaded_env_dir
634
649
 
635
650
  load_env_files(env=reload_env, override=reload_override, env_dir=reload_env_dir)
636
651
 
@@ -640,6 +655,9 @@ class DotEnvConfig(metaclass=ConfigMeta):
640
655
  logger.info(f"{self.__class__.__name__} configuration reloaded successfully")
641
656
  logger.debug(f"Reloaded fields: {', '.join(self._fields.keys())}")
642
657
 
658
+ self._load_env = reload_env
659
+ self._load_override = reload_override
660
+ self._load_env_dir = reload_env_dir
643
661
  return self
644
662
 
645
663
  @classmethod
@@ -709,8 +727,8 @@ class DotEnvConfig(metaclass=ConfigMeta):
709
727
  calls `load()`, the rest block and receive the same instance. Subsequent
710
728
  calls (from any thread) return the cached instance immediately without
711
729
  re-reading the environment, ignoring any arguments passed after the first
712
- call (a warning is logged if non-default arguments are passed against an
713
- already-warm cache).
730
+ call (a warning is logged if arguments that disagree with the ones
731
+ that populated the cache are passed against an already-warm cache).
714
732
 
715
733
  The cached instance is stored as a private class attribute on the config
716
734
  class itself (not in a module-level registry), so its lifetime is tied
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dotenvmodel"
7
- version = "0.6.1"
7
+ version = "0.6.3"
8
8
  description = "Type-safe environment configuration with automatic .env file loading"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -162,8 +162,10 @@ class TestCached:
162
162
  assert_type(config, MyConfig)
163
163
  assert config.value == "typed"
164
164
 
165
- def test_warning_logged_when_non_default_args_on_warm_cache(self, monkeypatch, caplog) -> None:
166
- """cached() logs a warning when non-default args are passed against a warm cache."""
165
+ def test_warning_logged_when_args_disagree_with_the_warm_cache(
166
+ self, monkeypatch, caplog
167
+ ) -> None:
168
+ """cached() warns when a caller's args differ from the ones that populated the cache."""
167
169
 
168
170
  class Config(DotEnvConfig):
169
171
  value: str = Field(default="x")
@@ -176,6 +178,130 @@ class TestCached:
176
178
  assert result is Config.cached()
177
179
  assert any("arguments were ignored" in r.message for r in caplog.records)
178
180
 
181
+ def test_repeating_the_arguments_the_cache_was_built_with_is_silent(
182
+ self, monkeypatch, caplog
183
+ ) -> None:
184
+ """Asking for what you already got is not a mistake and must not be reported as one.
185
+
186
+ The motivating case is an application accessor that must pass ``override=False`` (the
187
+ process environment beats ``.env`` files, per 12-factor) on every call. Warning it every
188
+ time emits a line per settings access for entirely correct usage — and a warning that
189
+ fires on correct usage is one readers learn to filter out, costing it the job it exists
190
+ for, which ``test_warning_logged_when_args_disagree_with_the_warm_cache`` still covers.
191
+
192
+ Asserts on WHETHER anything was reported, never on the wording: the promise is silence,
193
+ not a particular sentence, and a reworded message must not fail this test.
194
+ """
195
+
196
+ class Config(DotEnvConfig):
197
+ value: str = Field(default="x")
198
+
199
+ args = {"env": "prod", "override": False, "env_dir": Path("/tmp")}
200
+ first = Config.cached(**args)
201
+
202
+ # Window opened AFTER the populating call, so only the repeats are observed.
203
+ with caplog.at_level("WARNING", logger="dotenvmodel"):
204
+ caplog.clear()
205
+ second = Config.cached(**args)
206
+ third = Config.cached(**args)
207
+ repeats_reported = list(caplog.records)
208
+
209
+ assert second is first
210
+ assert third is first
211
+ assert repeats_reported == []
212
+
213
+ def test_a_bare_reload_keeps_the_precedence_the_cache_was_loaded_with(
214
+ self, monkeypatch, caplog
215
+ ) -> None:
216
+ """The SIGHUP shape: ``reload()`` with no arguments must not revert to ``override=True``.
217
+
218
+ A hot-reload handler calls ``reload()`` bare. If that silently reset precedence to
219
+ "``.env`` files beat the process environment", a running service would flip to the
220
+ opposite configuration on a signal it was told was a no-op refresh — and an accessor
221
+ passing ``override=False`` would then also start warning, having never changed.
222
+ """
223
+
224
+ class Config(DotEnvConfig):
225
+ value: str = Field(default="x")
226
+
227
+ instance = Config.cached(override=False, env_dir=Path("/tmp"))
228
+ assert instance.loaded_with() == (None, False, Path("/tmp"))
229
+
230
+ instance.reload()
231
+
232
+ assert instance.loaded_with() == (None, False, Path("/tmp"))
233
+ with caplog.at_level("WARNING", logger="dotenvmodel"):
234
+ caplog.clear()
235
+ assert Config.cached(override=False, env_dir=Path("/tmp")) is instance
236
+ reported = list(caplog.records)
237
+ assert reported == []
238
+
239
+ def test_reload_with_new_env_updates_loaded_with(self, monkeypatch) -> None:
240
+ """``reload(env=...)`` must update ``loaded_with()`` so a subsequent bare reload reuses it.
241
+
242
+ Without this, ``reload(env="prod")`` leaves ``loaded_with()`` reporting the original
243
+ env, a bare ``reload()`` silently reverts to it, and ``cached()`` warns on the
244
+ env the instance actually holds because ``loaded_with()`` still says the old one.
245
+ """
246
+
247
+ class Config(DotEnvConfig):
248
+ value: str = Field(default="x")
249
+
250
+ instance = Config.load(env="dev", override=False)
251
+ assert instance.loaded_with() == ("dev", False, None)
252
+
253
+ instance.reload(env="prod")
254
+
255
+ assert instance.loaded_with() == ("prod", False, None)
256
+
257
+ instance.reload()
258
+
259
+ assert instance.loaded_with() == ("prod", False, None)
260
+
261
+ def test_a_reset_cache_is_not_judged_against_the_arguments_it_dropped(
262
+ self, monkeypatch, caplog
263
+ ) -> None:
264
+ """After ``reset_cached()`` the next call repopulates, so it cannot disagree with anything."""
265
+
266
+ class Config(DotEnvConfig):
267
+ value: str = Field(default="x")
268
+
269
+ Config.cached(override=False)
270
+ Config.reset_cached()
271
+ # Repopulate OUTSIDE the window: a real load emits its own unrelated warnings (e.g. "no
272
+ # .env files found"), and this test is about the warm path, not about loading.
273
+ Config.cached(env="prod")
274
+
275
+ with caplog.at_level("WARNING", logger="dotenvmodel"):
276
+ caplog.clear()
277
+ Config.cached(env="prod")
278
+ reported = list(caplog.records)
279
+
280
+ assert reported == []
281
+
282
+ def test_a_scoped_override_leaves_the_warm_cache_judged_as_before(
283
+ self, monkeypatch, caplog
284
+ ) -> None:
285
+ """Exiting ``cached_override()`` restores the pre-override state, warnings included."""
286
+
287
+ class Config(DotEnvConfig):
288
+ value: str = Field(default="x")
289
+
290
+ Config.cached(override=False)
291
+ with Config.cached_override(Config.load_from_dict({"VALUE": "y"})):
292
+ pass
293
+
294
+ with caplog.at_level("WARNING", logger="dotenvmodel"):
295
+ caplog.clear()
296
+ Config.cached(override=False)
297
+ agreeing = list(caplog.records)
298
+ caplog.clear()
299
+ Config.cached(env="prod")
300
+ disagreeing = list(caplog.records)
301
+
302
+ assert agreeing == []
303
+ assert len(disagreeing) == 1
304
+
179
305
  def test_thread_safety_concurrent_first_access(self, monkeypatch) -> None:
180
306
  """Concurrent first callers all receive the same single instance."""
181
307
  import time
@@ -245,7 +245,7 @@ wheels = [
245
245
 
246
246
  [[package]]
247
247
  name = "dotenvmodel"
248
- version = "0.6.1"
248
+ version = "0.6.3"
249
249
  source = { editable = "." }
250
250
  dependencies = [
251
251
  { name = "python-dotenv" },
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.6.1"
3
- }
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