dotenvmodel 0.5.4__tar.gz → 0.6.1__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.
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/.github/workflows/ci.yml +1 -1
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/.github/workflows/publish.yml +1 -1
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/.gitignore +3 -0
- dotenvmodel-0.6.1/.release-please-manifest.json +3 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/CHANGELOG.md +19 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/Makefile +4 -3
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/PKG-INFO +24 -2
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/README.md +23 -1
- dotenvmodel-0.6.1/docs/api-reference/caching.md +7 -0
- dotenvmodel-0.6.1/docs/guides/loading.md +301 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/dotenvmodel/__init__.py +4 -1
- dotenvmodel-0.6.1/dotenvmodel/caching.py +265 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/dotenvmodel/config.py +224 -2
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/mkdocs.yml +1 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/pyproject.toml +1 -1
- dotenvmodel-0.6.1/tests/conftest.py +83 -0
- dotenvmodel-0.6.1/tests/test_cached.py +978 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/uv.lock +1 -1
- dotenvmodel-0.5.4/.release-please-manifest.json +0 -3
- dotenvmodel-0.5.4/docs/guides/loading.md +0 -209
- dotenvmodel-0.5.4/tests/conftest.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/.github/CODEOWNERS +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/.github/dependabot.yml +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/.github/pull_request_template.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/.github/workflows/docs.yml +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/.github/workflows/release-please.yml +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/.python-version +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/CODE_OF_CONDUCT.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/CONTRIBUTING.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/LICENSE +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/SECURITY.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/api-reference/coercion.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/api-reference/config.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/api-reference/constants.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/api-reference/describe-formatters.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/api-reference/describe-renderers.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/api-reference/describe.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/api-reference/dotenvmodel.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/api-reference/exceptions.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/api-reference/fields.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/api-reference/loading.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/api-reference/logging-config.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/api-reference/metaclass.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/api-reference/types.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/api-reference/validation.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/changelog.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/examples/complete-app.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/getting-started/installation.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/getting-started/quick-start.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/guides/configuration-docs.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/guides/error-handling.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/guides/fields.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/guides/logging.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/guides/prefixes.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/guides/types.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/guides/validation.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/docs/index.md +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/dotenvmodel/_constants.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/dotenvmodel/_redaction.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/dotenvmodel/coercion.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/dotenvmodel/describe/__init__.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/dotenvmodel/describe/formatters.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/dotenvmodel/describe/renderers.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/dotenvmodel/exceptions.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/dotenvmodel/fields.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/dotenvmodel/loading.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/dotenvmodel/logging_config.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/dotenvmodel/metaclass.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/dotenvmodel/py.typed +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/dotenvmodel/types.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/dotenvmodel/validation.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/examples/advanced_types.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/examples/basic_usage.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/examples/describe_documentation.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/examples/generate_env_example.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/examples/logging_example.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/release-please-config.json +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/__init__.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_basic.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_coercion.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_collection_validators.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_defaults_coercion.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_describe.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_edge_adoption.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_edge_datetime.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_edge_describe_output.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_edge_enum.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_edge_field_options.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_edge_numbers.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_edge_path_options.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_edge_strings.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_empty_collection_items.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_empty_strings.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_enum.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_errors.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_field_constraints.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_future_annotations.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_inheritance.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_json.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_json_and_optional.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_loading.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_logging_config.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_nested_config.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_post_load.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_prefix.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_reload.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_secret_redaction.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_secretstr_security.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_string_affixes.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_strip.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_strip_integration.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_types.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_union_types.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_url_dsn.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_url_password_decoding.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_url_unquote.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_urls.py +0 -0
- {dotenvmodel-0.5.4 → dotenvmodel-0.6.1}/tests/test_validator.py +0 -0
|
@@ -5,6 +5,25 @@ 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.1](https://github.com/AZX-PBC-OSS/dotenvmodel/compare/v0.6.0...v0.6.1) (2026-07-27)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* eliminate cross-class reentrancy deadlock in cached() locking ([#52](https://github.com/AZX-PBC-OSS/dotenvmodel/issues/52)) ([592297a](https://github.com/AZX-PBC-OSS/dotenvmodel/commit/592297a1823b2c26b00f974882834e5ace8bc968))
|
|
14
|
+
|
|
15
|
+
## [0.6.0](https://github.com/AZX-PBC-OSS/dotenvmodel/compare/v0.5.4...v0.6.0) (2026-07-27)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* add cached() / reset_cached() / cached_override() singleton accessor ([#51](https://github.com/AZX-PBC-OSS/dotenvmodel/issues/51)) ([638f8cc](https://github.com/AZX-PBC-OSS/dotenvmodel/commit/638f8cc8515398ad983bc321dd906effe4df17f8))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Continuous Integration
|
|
24
|
+
|
|
25
|
+
* bump setup-uv to v9.0.0 ([#48](https://github.com/AZX-PBC-OSS/dotenvmodel/issues/48)) ([21bcc4f](https://github.com/AZX-PBC-OSS/dotenvmodel/commit/21bcc4f96d16a1e075d3c59ba3291cb1f9af32d6))
|
|
26
|
+
|
|
8
27
|
## [0.5.4](https://github.com/AZX-PBC-OSS/dotenvmodel/compare/v0.5.3...v0.5.4) (2026-07-22)
|
|
9
28
|
|
|
10
29
|
|
|
@@ -4,9 +4,9 @@ help:
|
|
|
4
4
|
@echo "Available commands:"
|
|
5
5
|
@echo " make install - Install package and dev dependencies"
|
|
6
6
|
@echo " make test - Run tests with coverage"
|
|
7
|
-
@echo " make lint - Run ruff linter"
|
|
7
|
+
@echo " make lint - Run ruff linter and formatting check (matches CI)"
|
|
8
8
|
@echo " make format - Format code with ruff"
|
|
9
|
-
@echo " make type-check - Run pyright type checker"
|
|
9
|
+
@echo " make type-check - Run pyright type checker (dotenvmodel + tests)"
|
|
10
10
|
@echo " make clean - Clean build artifacts"
|
|
11
11
|
@echo " make build - Build package"
|
|
12
12
|
@echo " make publish - Publish to PyPI"
|
|
@@ -19,13 +19,14 @@ test:
|
|
|
19
19
|
|
|
20
20
|
lint:
|
|
21
21
|
uv run ruff check .
|
|
22
|
+
uv run ruff format --check .
|
|
22
23
|
|
|
23
24
|
format:
|
|
24
25
|
uv run ruff format .
|
|
25
26
|
uv run ruff check --fix .
|
|
26
27
|
|
|
27
28
|
type-check:
|
|
28
|
-
uv run pyright dotenvmodel
|
|
29
|
+
uv run pyright dotenvmodel tests
|
|
29
30
|
|
|
30
31
|
clean:
|
|
31
32
|
rm -rf build/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotenvmodel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
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
|
|
@@ -1357,6 +1357,27 @@ def test_with_fixture(test_config):
|
|
|
1357
1357
|
assert test_config.database_url == "sqlite:///:memory:"
|
|
1358
1358
|
```
|
|
1359
1359
|
|
|
1360
|
+
The `load_from_dict()` examples above test a config's field-parsing logic directly. If your application code calls `AppConfig.cached()` — the process-wide singleton — one test's cached instance will leak into the next. Use `cached_override()` for a scoped, self-restoring per-test override (the recommended approach, structurally similar to Django's `override_settings`):
|
|
1361
|
+
|
|
1362
|
+
```python
|
|
1363
|
+
def test_with_custom_config():
|
|
1364
|
+
test_config = AppConfig.load_from_dict({"DATABASE_URL": "postgresql://localhost/test"})
|
|
1365
|
+
with AppConfig.cached_override(test_config):
|
|
1366
|
+
assert AppConfig.cached() is test_config
|
|
1367
|
+
# Previous cached() state is automatically restored here — even if the test raised.
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1370
|
+
For a blanket teardown between test modules, use `reset_cached()` in an `autouse` fixture:
|
|
1371
|
+
|
|
1372
|
+
```python
|
|
1373
|
+
@pytest.fixture(autouse=True)
|
|
1374
|
+
def reset_config_cache():
|
|
1375
|
+
yield
|
|
1376
|
+
AppConfig.reset_cached()
|
|
1377
|
+
```
|
|
1378
|
+
|
|
1379
|
+
See the [Caching guide](docs/guides/loading.md#caching-a-singleton-instance) for the full lazy/thread-safe singleton pattern, test idioms, and guidance on when `cached()` is appropriate vs. dependency injection.
|
|
1380
|
+
|
|
1360
1381
|
## Best Practices
|
|
1361
1382
|
|
|
1362
1383
|
1. **Use Type Hints**: Always specify type hints for proper validation
|
|
@@ -1401,9 +1422,10 @@ def test_with_fixture(test_config):
|
|
|
1401
1422
|
|
|
1402
1423
|
`DotEnvConfig` instances are **not thread-safe** during `reload()`. If multiple threads call `reload()` on the same instance concurrently, field values may become inconsistent. In multi-threaded server environments (FastAPI, gunicorn, etc.):
|
|
1403
1424
|
|
|
1404
|
-
- Call `load()` once at startup and share the immutable instance
|
|
1425
|
+
- Call `load()` once at startup and share the immutable instance, or use `cached()` for a built-in lazy, thread-safe singleton that does this automatically
|
|
1405
1426
|
- If you need to reload, use a lock or create a new instance via `load()` instead of calling `reload()` on a shared instance
|
|
1406
1427
|
- Avoid calling `reload()` while request handlers are reading config values
|
|
1428
|
+
- In tests that need different configurations per test, use `cached_override()` for a scoped, self-restoring override, or `reset_cached()` in a fixture for a blanket teardown between test modules
|
|
1407
1429
|
|
|
1408
1430
|
### Union Types (Non-Optional)
|
|
1409
1431
|
|
|
@@ -1329,6 +1329,27 @@ def test_with_fixture(test_config):
|
|
|
1329
1329
|
assert test_config.database_url == "sqlite:///:memory:"
|
|
1330
1330
|
```
|
|
1331
1331
|
|
|
1332
|
+
The `load_from_dict()` examples above test a config's field-parsing logic directly. If your application code calls `AppConfig.cached()` — the process-wide singleton — one test's cached instance will leak into the next. Use `cached_override()` for a scoped, self-restoring per-test override (the recommended approach, structurally similar to Django's `override_settings`):
|
|
1333
|
+
|
|
1334
|
+
```python
|
|
1335
|
+
def test_with_custom_config():
|
|
1336
|
+
test_config = AppConfig.load_from_dict({"DATABASE_URL": "postgresql://localhost/test"})
|
|
1337
|
+
with AppConfig.cached_override(test_config):
|
|
1338
|
+
assert AppConfig.cached() is test_config
|
|
1339
|
+
# Previous cached() state is automatically restored here — even if the test raised.
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
For a blanket teardown between test modules, use `reset_cached()` in an `autouse` fixture:
|
|
1343
|
+
|
|
1344
|
+
```python
|
|
1345
|
+
@pytest.fixture(autouse=True)
|
|
1346
|
+
def reset_config_cache():
|
|
1347
|
+
yield
|
|
1348
|
+
AppConfig.reset_cached()
|
|
1349
|
+
```
|
|
1350
|
+
|
|
1351
|
+
See the [Caching guide](docs/guides/loading.md#caching-a-singleton-instance) for the full lazy/thread-safe singleton pattern, test idioms, and guidance on when `cached()` is appropriate vs. dependency injection.
|
|
1352
|
+
|
|
1332
1353
|
## Best Practices
|
|
1333
1354
|
|
|
1334
1355
|
1. **Use Type Hints**: Always specify type hints for proper validation
|
|
@@ -1373,9 +1394,10 @@ def test_with_fixture(test_config):
|
|
|
1373
1394
|
|
|
1374
1395
|
`DotEnvConfig` instances are **not thread-safe** during `reload()`. If multiple threads call `reload()` on the same instance concurrently, field values may become inconsistent. In multi-threaded server environments (FastAPI, gunicorn, etc.):
|
|
1375
1396
|
|
|
1376
|
-
- Call `load()` once at startup and share the immutable instance
|
|
1397
|
+
- Call `load()` once at startup and share the immutable instance, or use `cached()` for a built-in lazy, thread-safe singleton that does this automatically
|
|
1377
1398
|
- If you need to reload, use a lock or create a new instance via `load()` instead of calling `reload()` on a shared instance
|
|
1378
1399
|
- Avoid calling `reload()` while request handlers are reading config values
|
|
1400
|
+
- In tests that need different configurations per test, use `cached_override()` for a scoped, self-restoring override, or `reset_cached()` in a fixture for a blanket teardown between test modules
|
|
1379
1401
|
|
|
1380
1402
|
### Union Types (Non-Optional)
|
|
1381
1403
|
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Loading Configuration
|
|
2
|
+
|
|
3
|
+
dotenvmodel provides flexible configuration loading from environment variables and `.env` files with Node.js-style cascading. This guide covers all loading methods and their parameters.
|
|
4
|
+
|
|
5
|
+
## .env File Cascading
|
|
6
|
+
|
|
7
|
+
When you call `load()`, dotenvmodel automatically reads `.env` files in a cascading order. Later files override earlier ones, giving you layered configuration from shared base values to local overrides.
|
|
8
|
+
|
|
9
|
+
!!! info "Cascade Order"
|
|
10
|
+
|
|
11
|
+
Files are loaded in the following order (later files override earlier ones):
|
|
12
|
+
|
|
13
|
+
1. **`.env`** — Base configuration (usually gitignored)
|
|
14
|
+
2. **`.env.local`** — Local base overrides (gitignored, never committed)
|
|
15
|
+
3. **`.env.{env}`** — Environment-specific (committed to repo)
|
|
16
|
+
4. **`.env.{env}.local`** — Local environment overrides (gitignored, never committed)
|
|
17
|
+
|
|
18
|
+
### Practical Example
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# .env (base - usually gitignored)
|
|
22
|
+
DATABASE_URL=postgresql://localhost/myapp
|
|
23
|
+
REDIS_URL=redis://localhost:6379
|
|
24
|
+
DEBUG=false
|
|
25
|
+
|
|
26
|
+
# .env.local (local base overrides - gitignored)
|
|
27
|
+
DATABASE_URL=postgresql://localhost/myapp_local
|
|
28
|
+
|
|
29
|
+
# .env.dev (development - committed to repo)
|
|
30
|
+
DEBUG=true
|
|
31
|
+
LOG_LEVEL=DEBUG
|
|
32
|
+
|
|
33
|
+
# .env.dev.local (local dev overrides - gitignored)
|
|
34
|
+
ENABLE_PROFILING=true
|
|
35
|
+
API_KEY=dev-key-local-override
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
When you load with `env="dev"`:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
config = AppConfig.load(env="dev")
|
|
42
|
+
# Loads in order: .env -> .env.local -> .env.dev -> .env.dev.local
|
|
43
|
+
# Final DATABASE_URL: postgresql://localhost/myapp_local (from .env.local)
|
|
44
|
+
# Final DEBUG: true (from .env.dev)
|
|
45
|
+
# Final ENABLE_PROFILING: true (from .env.dev.local)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
!!! tip "Committing .env files"
|
|
49
|
+
|
|
50
|
+
- **Commit**: `.env.{env}` files (e.g., `.env.dev`, `.env.prod`) — shared defaults
|
|
51
|
+
- **Gitignore**: `.env`, `.env.local`, `.env.{env}.local` — contain secrets and local overrides
|
|
52
|
+
|
|
53
|
+
## The `env` Parameter
|
|
54
|
+
|
|
55
|
+
The `env` parameter selects which environment-specific files to load. If not provided, it reads from the `ENV` environment variable, defaulting to `"dev"`.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# Auto-detect from ENV environment variable, default "dev"
|
|
59
|
+
config = AppConfig.load()
|
|
60
|
+
|
|
61
|
+
# Explicit environment
|
|
62
|
+
config = AppConfig.load(env="prod")
|
|
63
|
+
|
|
64
|
+
# Test environment
|
|
65
|
+
config = AppConfig.load(env="test")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
!!! warning "Valid environment names"
|
|
69
|
+
|
|
70
|
+
Environment names must only contain alphanumeric characters, hyphens, and underscores. This prevents path traversal attacks. Invalid names raise `ValueError`.
|
|
71
|
+
|
|
72
|
+
## The `override` Parameter
|
|
73
|
+
|
|
74
|
+
Controls whether `.env` file values override existing environment variables.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# .env files override env vars (default)
|
|
78
|
+
config = AppConfig.load(override=True)
|
|
79
|
+
|
|
80
|
+
# Env vars take precedence over .env files
|
|
81
|
+
config = AppConfig.load(override=False)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| `override` | Behavior |
|
|
85
|
+
|------------|----------|
|
|
86
|
+
| `True` (default) | `.env` file values override existing environment variables |
|
|
87
|
+
| `False` | Existing environment variables take precedence over `.env` files |
|
|
88
|
+
|
|
89
|
+
!!! tip "When to use `override=False`"
|
|
90
|
+
|
|
91
|
+
Use `override=False` in containerized environments where you inject config via environment variables and want them to take precedence over any `.env` files that might exist in the image.
|
|
92
|
+
|
|
93
|
+
## The `env_dir` Parameter
|
|
94
|
+
|
|
95
|
+
By default, dotenvmodel looks for `.env` files in the current working directory. You can specify a custom directory.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from pathlib import Path
|
|
99
|
+
|
|
100
|
+
# Custom .env file directory
|
|
101
|
+
config = AppConfig.load(env_dir=Path("/app/config"))
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `DOTENV_DIR` Environment Variable
|
|
105
|
+
|
|
106
|
+
If `env_dir` is not provided, dotenvmodel checks the `DOTENV_DIR` environment variable:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Set via environment variable
|
|
110
|
+
export DOTENV_DIR=/app/config
|
|
111
|
+
python your_app.py
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# No env_dir needed — reads from DOTENV_DIR
|
|
116
|
+
config = AppConfig.load()
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
!!! note "Precedence"
|
|
120
|
+
|
|
121
|
+
The `env_dir` parameter takes precedence over the `DOTENV_DIR` environment variable, which takes precedence over the current working directory.
|
|
122
|
+
|
|
123
|
+
## Loading from a Dictionary
|
|
124
|
+
|
|
125
|
+
Use `load_from_dict()` for testing or when you have config values from a non-environment source. This bypasses `.env` file loading entirely.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
# Load from dictionary for testing
|
|
129
|
+
config = AppConfig.load_from_dict({
|
|
130
|
+
"DATABASE_URL": "postgresql://localhost/test",
|
|
131
|
+
"API_KEY": "test-key",
|
|
132
|
+
"DEBUG": "true",
|
|
133
|
+
"PORT": "8000",
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
# Skip validation if needed
|
|
137
|
+
config = AppConfig.load_from_dict(data, validate=False)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
!!! tip "Keys can be field names or env var names"
|
|
141
|
+
|
|
142
|
+
Dictionary keys can be either the env var name (e.g., `"DATABASE_URL"`) or the field name (e.g., `"database_url"`). Env var names take precedence.
|
|
143
|
+
|
|
144
|
+
!!! warning "Use `load()` in production"
|
|
145
|
+
|
|
146
|
+
`load_from_dict()` is designed for testing. In production, always use `load()` to read from environment variables and `.env` files.
|
|
147
|
+
|
|
148
|
+
## Reloading Configuration
|
|
149
|
+
|
|
150
|
+
The `reload()` method lets you refresh configuration at runtime without creating a new instance. This is useful for picking up environment changes or switching environments.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
import os
|
|
154
|
+
|
|
155
|
+
# Load initial configuration
|
|
156
|
+
config = AppConfig.load(env="dev")
|
|
157
|
+
print(config.port) # 8000
|
|
158
|
+
|
|
159
|
+
# Later, when environment variables change...
|
|
160
|
+
os.environ["PORT"] = "9000"
|
|
161
|
+
|
|
162
|
+
# Reload the configuration
|
|
163
|
+
config.reload()
|
|
164
|
+
print(config.port) # 9000
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Reusing Original Parameters
|
|
168
|
+
|
|
169
|
+
By default, `reload()` reuses the same `env`, `override`, and `env_dir` from the original `load()` call:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
config = AppConfig.load(env="dev", override=True)
|
|
173
|
+
config.reload() # Uses env="dev", override=True
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Overriding Parameters During Reload
|
|
177
|
+
|
|
178
|
+
You can override any parameter by passing new values:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
# Switch to production environment
|
|
182
|
+
config.reload(env="prod")
|
|
183
|
+
|
|
184
|
+
# Change override behavior
|
|
185
|
+
config.reload(override=False)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
!!! warning "A failed reload can leave the instance partially reloaded"
|
|
189
|
+
|
|
190
|
+
Fields are reloaded onto the same instance one at a time, so if validation (or the `post_load` hook) fails mid-reload, fields already reloaded keep their new values while the rest keep the old. If you catch reload errors, treat the instance as suspect — or build a fresh one with `load()` instead.
|
|
191
|
+
|
|
192
|
+
!!! info "reload() returns the same instance"
|
|
193
|
+
|
|
194
|
+
`reload()` returns `self`, making it useful for method chaining:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
config.reload(env="prod").port
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
!!! warning "Thread safety"
|
|
201
|
+
|
|
202
|
+
`DotEnvConfig` instances are **not thread-safe** during `reload()`. In multi-threaded environments, use a lock or create a new instance via `load()` instead of calling `reload()` on a shared instance.
|
|
203
|
+
|
|
204
|
+
## Caching a Singleton Instance
|
|
205
|
+
|
|
206
|
+
Application code often needs a single shared config instance that is loaded once and reused everywhere. `cached()` provides this as a built-in: it loads on first call and returns the same instance on every subsequent call, with no re-reading of the environment.
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
class AppConfig(DotEnvConfig):
|
|
210
|
+
database_url: str = Field()
|
|
211
|
+
port: int = Field(default=8000)
|
|
212
|
+
|
|
213
|
+
# First call loads from the environment.
|
|
214
|
+
config = AppConfig.cached()
|
|
215
|
+
|
|
216
|
+
# Subsequent calls return the same instance — no re-read.
|
|
217
|
+
same_config = AppConfig.cached()
|
|
218
|
+
assert config is same_config
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Lazy and Thread-Safe
|
|
222
|
+
|
|
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
|
+
|
|
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.
|
|
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.
|
|
228
|
+
|
|
229
|
+
!!! warning "Reentrant `cached()` calls raise `RuntimeError`"
|
|
230
|
+
|
|
231
|
+
Calling `cached()` reentrantly for the same class from within that class's own `load()` / `post_load()` / field `validator` hooks raises `RuntimeError`. The internal lock is reentrant, so the nested call would not deadlock — it would see a cold cache and recurse into `load()` without bound, which is why it is rejected. If a hook needs the config instance mid-load, use `self`, or call `cls.load()` directly with re-entry guarding (an unconditional `cls.load()` inside `post_load()` re-runs the hooks and recurses until `RecursionError`).
|
|
232
|
+
|
|
233
|
+
Calling `cached()`, `reset_cached()`, or `cached_override()` for **other** classes from those hooks is supported — for example, one class's `post_load()` may call `cached()` on another config class. However, calling `reset_cached()` or entering `cached_override()` for the **same** class whose first load is still in flight raises `RuntimeError` (the load installs its instance when it completes, which would silently undo the reset or discard the override). A circular cross-class hook chain (A's hook loads B, B's hook loads A) collapses back onto the first class and likewise raises `RuntimeError`.
|
|
234
|
+
|
|
235
|
+
Hooks must not block on another thread (e.g. `thread.join()`) that touches the cache: the internal lock is held for the duration of a load, so the joining thread would hold the lock while the joined thread waits for it — a deadlock the reentrant lock cannot prevent.
|
|
236
|
+
|
|
237
|
+
### Scoped Overrides for Tests
|
|
238
|
+
|
|
239
|
+
`cached_override()` is a context manager that temporarily replaces the cached instance for the duration of a `with` block and automatically restores the previous state on exit — even if the block raises an exception. This is the **primary recommended tool** for test isolation when a single test needs a different config: it is structurally the same shape as Django's `override_settings` — scoped, self-restoring, and failure-safe.
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
def test_with_custom_config():
|
|
243
|
+
test_config = AppConfig.load_from_dict({"DATABASE_URL": "postgresql://localhost/test"})
|
|
244
|
+
with AppConfig.cached_override(test_config):
|
|
245
|
+
assert AppConfig.cached() is test_config
|
|
246
|
+
# Previous cached() state (or absence of one) is restored here.
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Because restoration is automatic and unconditional, `cached_override()` cannot forget to clean up — unlike a bare `reset_cached()` call that a test author might omit, silently leaking state into the next test. Reach for `cached_override()` first when a single test needs a different config; use `reset_cached()` (below) for the coarser case of a blanket fixture teardown between test modules.
|
|
250
|
+
|
|
251
|
+
!!! warning "Not for concurrent use"
|
|
252
|
+
|
|
253
|
+
`cached_override()` is not designed for use while other threads may concurrently call `cached()` on the same class. The override window is not synchronized against concurrent readers beyond the lock-protected set/restore operations, so overlapping a `cached_override()` block with genuinely concurrent cross-thread `cached()` calls is racy in terms of which threads observe the override vs. the restored value. No data corruption occurs, but the observed value is unspecified during the transition.
|
|
254
|
+
|
|
255
|
+
### Resetting the Cache for Tests
|
|
256
|
+
|
|
257
|
+
`reset_cached()` is the coarser fallback: it unconditionally clears this class's cached instance so the next `cached()` call will call `load()` again. It is useful as a blanket fixture teardown — for example, clearing everything between test modules — but `cached_override()` (above) should be reached for first when a single test needs a different config, because `reset_cached()` cannot auto-restore and a forgotten call leaks state.
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
import pytest
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@pytest.fixture(autouse=True)
|
|
264
|
+
def reset_config_cache():
|
|
265
|
+
yield
|
|
266
|
+
AppConfig.reset_cached()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_dev_config(monkeypatch):
|
|
270
|
+
monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/dev")
|
|
271
|
+
config = AppConfig.cached()
|
|
272
|
+
assert "dev" in config.database_url
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_prod_config(monkeypatch):
|
|
276
|
+
monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/prod")
|
|
277
|
+
config = AppConfig.cached()
|
|
278
|
+
assert "prod" in config.database_url
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`reset_cached()` only affects the exact class it is called on — other `DotEnvConfig` subclasses keep their own cached instances.
|
|
282
|
+
|
|
283
|
+
!!! note "Per-class isolation"
|
|
284
|
+
|
|
285
|
+
The cache is keyed by the exact class object. `SubA.cached()` and `SubB.cached()` cache independently, and a subclass of a subclass does not inherit its parent's cached instance.
|
|
286
|
+
|
|
287
|
+
### Not a Substitute for Dependency Injection
|
|
288
|
+
|
|
289
|
+
`cached()` is a single-config-per-process convenience for the common case where there is no DI framework already in place — scripts, simple services, cases where threading a config parameter through every call is impractical. It is **not** a substitute for dependency injection in applications that already have one.
|
|
290
|
+
|
|
291
|
+
- **pydantic-settings** deliberately does not ship a singleton/caching accessor itself; the maintainers' position (see [pydantic/pydantic-settings#410](https://github.com/pydantic/pydantic-settings/issues/410)) is that singleton lifecycle is an application concern, not the settings library's.
|
|
292
|
+
- **FastAPI**'s own documented settings pattern uses `functools.lru_cache` to avoid re-loading, but the load-bearing mechanism for *testability* is `app.dependency_overrides` — FastAPI's docs steer people toward DI-based overriding for tests, with the cache being an optimization, not the override mechanism.
|
|
293
|
+
|
|
294
|
+
**Conclusion:** if your application already has an app/request object and a DI mechanism (FastAPI, Flask with a DI extension, `svcs`-style service locators, etc.), prefer injecting the loaded config instance through that mechanism rather than calling `cached()` from deep in the call stack. `cached()` and `cached_override()` exist for the case where there is no DI framework to thread the config through — not to encourage a global-singleton-everywhere style in apps that already have DI.
|
|
295
|
+
|
|
296
|
+
## See Also
|
|
297
|
+
|
|
298
|
+
- [Loading API Reference](../api-reference/loading.md) — `load_env_files()`, `get_env_var()`, `get_env_var_name()`
|
|
299
|
+
- [DotEnvConfig API Reference](../api-reference/config.md) — `load()`, `reload()`, `load_from_dict()`, `cached()`, `reset_cached()`, `cached_override()`
|
|
300
|
+
- [Field Definitions](fields.md) — Defining config fields with `Field()`
|
|
301
|
+
- [Validation](validation.md) — Constraint validation
|
|
@@ -23,6 +23,9 @@ Public API:
|
|
|
23
23
|
- `ValidatorContext`: Context passed to `Field(validator=...)` hooks
|
|
24
24
|
- `DotEnvConfig.post_load`: Model-level hook for cross-field validation
|
|
25
25
|
and normalization after loading
|
|
26
|
+
- `DotEnvConfig.cached` / `DotEnvConfig.reset_cached` / `DotEnvConfig.cached_override`:
|
|
27
|
+
Process-wide thread-safe singleton accessor, its reset, and a scoped
|
|
28
|
+
self-restoring override for test isolation
|
|
26
29
|
- `SecretStr`: String type that hides values in logs
|
|
27
30
|
- `HttpUrl`, `PostgresDsn`, `RedisDsn`: URL/DSN types with validation
|
|
28
31
|
- `Json`: Type for parsing JSON strings
|
|
@@ -34,7 +37,7 @@ Public API:
|
|
|
34
37
|
Exception hierarchy
|
|
35
38
|
"""
|
|
36
39
|
|
|
37
|
-
__version__ = "0.
|
|
40
|
+
__version__ = "0.6.1" # x-release-please-version
|
|
38
41
|
__author__ = "AZX, PBC."
|
|
39
42
|
__email__ = "oss@azx.io"
|
|
40
43
|
__license__ = "MIT"
|