python-neva 5.1.0__tar.gz → 5.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_neva-5.1.0 → python_neva-5.3.0}/CHANGELOG.md +16 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/PKG-INFO +1 -1
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/service-providers.md +9 -4
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/testing.md +30 -3
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/testing/test_case.py +40 -4
- {python_neva-5.1.0 → python_neva-5.3.0}/pyproject.toml +1 -1
- python_neva-5.3.0/tests/testing/test_boot_application.py +85 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/uv.lock +1 -1
- {python_neva-5.1.0 → python_neva-5.3.0}/.claude/settings.local.json +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/.envrc +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/.gitignore +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/.gitlab-ci.yml +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/.pre-commit-config.yaml +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/.python-version +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/CLAUDE.md +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/README.md +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/application.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/config.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/facade.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/integrations/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/integrations/faststream.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/markers.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/py.typed +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/scopes.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/service_provider.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/config/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/config/base_providers.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/config/loader.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/config/py.typed +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/config/repository.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/database/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/database/config.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/database/connection.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/database/manager.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/database/provider.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/database/py.typed +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/database/transaction.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/contracts/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/contracts/dispatcher.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/contracts/event.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/contracts/handler.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/contracts/listener.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/dispatcher.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/event.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/event_registry.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/listener.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/policy.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/provider.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/py.typed +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/configuration.md +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/database-transactions.md +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/events.md +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/facades.md +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/factories.md +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/observability.md +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/result-option.md +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/security.md +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/manifest.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/py.typed +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/config.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/logging/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/logging/channels.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/logging/contracts.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/logging/manager.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/logging/provider.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/logging/resolver.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/py.typed +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/polyfactory/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/polyfactory/factories.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/polyfactory/persistence.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/polyfactory/py.typed +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/encryption/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/encryption/encrypter.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/encryption/protocol.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/hashing/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/hashing/config.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/hashing/hash_manager.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/hashing/hashers/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/hashing/hashers/argon2.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/hashing/hashers/protocol.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/provider.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/py.typed +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/tokens/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/tokens/generate_token.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/tokens/hash_token.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/tokens/verify_token.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/accessors.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/app.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/app.pyi +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/config.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/config.pyi +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/crypt.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/crypt.pyi +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/db.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/db.pyi +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/event.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/event.pyi +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/hash.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/hash.pyi +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/log.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/log.pyi +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/py.typed +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/results.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/strategy.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/strconv.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/time.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/testing/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/testing/fakes.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/testing/fixtures.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/neva/testing/py.typed +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/ruff.toml +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/scripts/retag-with-changelog.sh +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_cache.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_config_shapes.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_context.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_extends.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_facade_resolution.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_facade_root_nesting.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_lifetimes.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_register_resolution.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_registration.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_scope.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/config/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/config/test_config_path_resolution.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/config/test_loader.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/config/test_repository.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/conftest.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_connection_manager.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_database_manager.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_detached_lifespan.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_edge_cases.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_multi_connection.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_sqlalchemy_integration.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_transaction.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_transaction_callbacks.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_transaction_context.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_transaction_registry.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/conftest.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_before_dispatch.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_binding.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_deferred.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_dispatch.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_event.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_function_listener.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_immediate.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_listen_on_parent_class.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_listener_wiring.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/obs/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/obs/conftest.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/obs/test_channels.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/obs/test_facade.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/obs/test_manager.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/obs/test_provider_hook.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/obs/test_resolver.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/polyfactory/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/polyfactory/test_model_factory.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/security/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/security/test_config_shapes.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/security/test_encrypter.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/security/test_hash_manager.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/security/test_tokens.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/support/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/support/test_results.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/support/test_strategy.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/testing/__init__.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/testing/test_application_reuse.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/testing/test_create_config_migration.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/testing/test_event_fake.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/testing/test_facade_restore.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/testing/test_fixtures.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/testing/test_refresh_database.py +0 -0
- {python_neva-5.1.0 → python_neva-5.3.0}/tests/testing/test_test_case.py +0 -0
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## 5.3.0 (2026-09-02)
|
|
2
|
+
|
|
3
|
+
### ✨ Features
|
|
4
|
+
|
|
5
|
+
- **testing**: promote the application hook TestCase builds through
|
|
6
|
+
|
|
7
|
+
### 📝💡 Documentation
|
|
8
|
+
|
|
9
|
+
- **guidelines**: say which of register's two checks is read first
|
|
10
|
+
|
|
11
|
+
## 5.2.0 (2026-09-02)
|
|
12
|
+
|
|
13
|
+
### ✨ Features
|
|
14
|
+
|
|
15
|
+
- **testing**: promote the application hook TestCase builds through
|
|
16
|
+
|
|
1
17
|
## 5.1.0 (2026-09-02)
|
|
2
18
|
|
|
3
19
|
### ✨ Features
|
|
@@ -152,10 +152,15 @@ request-scoped container to another framework wants that, not `container`.
|
|
|
152
152
|
|
|
153
153
|
## Registration is closed after boot
|
|
154
154
|
|
|
155
|
-
`register()`
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
`register()` **returns `Err` once the application has booted**: dishka validates the graph at
|
|
156
|
+
build time, and the booted container holds APP-scoped instances a rebuild would orphan. Declare
|
|
157
|
+
providers in config, or register before entering `lifespan()`.
|
|
158
|
+
|
|
159
|
+
One case slips past that, and the order is what makes it: the already-registered check is read
|
|
160
|
+
**first**, so re-registering a class the application already holds returns `Ok(existing)` even
|
|
161
|
+
booted. Nothing is rebuilt, so nothing is at risk. It does mean a test asserting the refusal
|
|
162
|
+
must pass a provider the application never registered: hand it one the config or the setup
|
|
163
|
+
already installed and the assertion comes back `Ok`, proving nothing.
|
|
159
164
|
|
|
160
165
|
## Typed config shapes
|
|
161
166
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: testing
|
|
3
3
|
title: Testing
|
|
4
|
-
requires: python-neva>=
|
|
5
|
-
triggers: [writing a test, TestCase, RefreshDatabase, faking a facade, database isolation in tests, create_config]
|
|
4
|
+
requires: python-neva>=5.2
|
|
5
|
+
triggers: [writing a test, TestCase, RefreshDatabase, faking a facade, database isolation in tests, create_config, boot_application]
|
|
6
6
|
priority: 60
|
|
7
|
-
verified_by: [tests/testing/test_test_case.py, tests/testing/test_application_reuse.py, tests/testing/test_create_config_migration.py, tests/testing/test_refresh_database.py, tests/testing/test_facade_restore.py, tests/testing/test_event_fake.py, tests/testing/test_fixtures.py]
|
|
7
|
+
verified_by: [tests/testing/test_test_case.py, tests/testing/test_boot_application.py, tests/testing/test_application_reuse.py, tests/testing/test_create_config_migration.py, tests/testing/test_refresh_database.py, tests/testing/test_facade_restore.py, tests/testing/test_event_fake.py, tests/testing/test_fixtures.py]
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Testing
|
|
@@ -66,6 +66,33 @@ naming the fix. There is no instance-method fallback and no deprecation path —
|
|
|
66
66
|
Omit the override entirely and the default writes a minimal `app.py` / `providers.py` into
|
|
67
67
|
`tmp_path`.
|
|
68
68
|
|
|
69
|
+
## boot_application picks the application, not just its config
|
|
70
|
+
|
|
71
|
+
`create_config` says what configuration the class runs on; `boot_application` says what
|
|
72
|
+
application runs over it, and which lifespan holds it open. Override it when the thing under
|
|
73
|
+
test **owns** an `Application` instead of being one — an HTTP app, whose own lifespan is what
|
|
74
|
+
seeds its per-request state — so entering `Application.lifespan()` would boot the container and
|
|
75
|
+
skip everything wrapped around it.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
class HttpTestCase(TestCase):
|
|
79
|
+
@override
|
|
80
|
+
@classmethod
|
|
81
|
+
@asynccontextmanager
|
|
82
|
+
async def boot_application(cls, config_path: Path) -> AsyncIterator[Application]:
|
|
83
|
+
webapp = App(config_path=config_path)
|
|
84
|
+
async with webapp.router.lifespan_context(webapp):
|
|
85
|
+
yield webapp.application
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Yield the application `self.app` and the facade root must point at. It is a **context manager**,
|
|
89
|
+
so `@classmethod` and `@asynccontextmanager` both belong on the override — a factory returning
|
|
90
|
+
an application would hand the lifespan back to `TestCase`. `FreshApplication` routes through the
|
|
91
|
+
same hook, once per test.
|
|
92
|
+
|
|
93
|
+
Registration order still holds: register providers and include routers *before* entering the
|
|
94
|
+
lifespan. `Application.register` returns `Err` once booted.
|
|
95
|
+
|
|
69
96
|
## RefreshDatabase
|
|
70
97
|
|
|
71
98
|
Mix it in **alongside** `TestCase`; it cannot boot an application alone.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
from collections.abc import AsyncIterator, Iterator
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import override
|
|
7
8
|
|
|
@@ -39,6 +40,10 @@ class TestCase:
|
|
|
39
40
|
|
|
40
41
|
A class that genuinely needs a fresh application per test -- one varying the
|
|
41
42
|
*shape* of its config rather than reading it -- inherits `FreshApplication`.
|
|
43
|
+
|
|
44
|
+
Two public override points: `create_config` for the configuration, and
|
|
45
|
+
`boot_application` for the application built over it and the lifespan it is
|
|
46
|
+
held open by.
|
|
42
47
|
"""
|
|
43
48
|
|
|
44
49
|
pytestmark = pytest.mark.asyncio(loop_scope="class")
|
|
@@ -103,6 +108,39 @@ class TestCase:
|
|
|
103
108
|
)
|
|
104
109
|
raise TypeError(msg)
|
|
105
110
|
|
|
111
|
+
@classmethod
|
|
112
|
+
@asynccontextmanager
|
|
113
|
+
async def boot_application(cls, config_path: Path) -> AsyncIterator[Application]:
|
|
114
|
+
"""Build the application under test and hold it booted.
|
|
115
|
+
|
|
116
|
+
The public override point for *what* a class tests and *how* it starts.
|
|
117
|
+
`create_config` decides the configuration; this decides the application.
|
|
118
|
+
Overriding it is how a downstream package tests something that owns an
|
|
119
|
+
`Application` rather than being one -- an HTTP app whose own lifespan has
|
|
120
|
+
to be the one entered, because that is what seeds its per-request state.
|
|
121
|
+
|
|
122
|
+
An override yields the `Application` the facades and `self.app` must point
|
|
123
|
+
at, from inside whatever lifespan it needs::
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
@asynccontextmanager
|
|
127
|
+
async def boot_application(cls, config_path):
|
|
128
|
+
webapp = App(config_path=config_path)
|
|
129
|
+
async with webapp.router.lifespan_context(webapp):
|
|
130
|
+
yield webapp.application
|
|
131
|
+
|
|
132
|
+
It is a context manager rather than a factory so that the lifespan stays
|
|
133
|
+
the subclass's to choose. A factory would leave `TestCase` entering
|
|
134
|
+
`Application.lifespan()` itself, which boots the container but skips any
|
|
135
|
+
lifespan wrapped around it.
|
|
136
|
+
|
|
137
|
+
Yields:
|
|
138
|
+
The booted application under test.
|
|
139
|
+
"""
|
|
140
|
+
app = Application(config_path=config_path)
|
|
141
|
+
async with app.lifespan():
|
|
142
|
+
yield app
|
|
143
|
+
|
|
106
144
|
@pytest_asyncio.fixture(scope="class", loop_scope="class")
|
|
107
145
|
@classmethod
|
|
108
146
|
async def _test_case_application(
|
|
@@ -113,8 +151,7 @@ class TestCase:
|
|
|
113
151
|
Yields:
|
|
114
152
|
Application instance with lifespan managed.
|
|
115
153
|
"""
|
|
116
|
-
|
|
117
|
-
async with app.lifespan():
|
|
154
|
+
async with cls.boot_application(_test_case_config) as app:
|
|
118
155
|
yield app
|
|
119
156
|
|
|
120
157
|
@pytest.fixture(autouse=True)
|
|
@@ -174,8 +211,7 @@ class FreshApplication(TestCase):
|
|
|
174
211
|
Yields:
|
|
175
212
|
A freshly built Application, torn down at the end of the test.
|
|
176
213
|
"""
|
|
177
|
-
|
|
178
|
-
async with app.lifespan():
|
|
214
|
+
async with self.boot_application(self._build_config(tmp_path)) as app:
|
|
179
215
|
yield app
|
|
180
216
|
|
|
181
217
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""`boot_application` is the public seam for the application a class tests.
|
|
2
|
+
|
|
3
|
+
`create_config` already lets a subclass decide the configuration. It could not decide
|
|
4
|
+
the *application*: `TestCase` built an `Application` itself and entered
|
|
5
|
+
`Application.lifespan()`, so a class testing something that merely **owns** an
|
|
6
|
+
application -- an HTTP app, whose own lifespan is the one that seeds its per-request
|
|
7
|
+
state -- had no way in but to override `_test_case_application`, a private fixture,
|
|
8
|
+
from another distribution.
|
|
9
|
+
|
|
10
|
+
The hook is a context manager rather than a factory precisely so the lifespan is the
|
|
11
|
+
subclass's to choose; the wrapper assertions below are what pins that down.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from collections.abc import AsyncIterator
|
|
15
|
+
from contextlib import asynccontextmanager
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import ClassVar, override
|
|
18
|
+
|
|
19
|
+
from neva.arch import Application, Facade
|
|
20
|
+
from neva.testing import FreshApplication, TestCase
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Owner:
|
|
24
|
+
"""Stands in for an object that owns an Application and wraps its lifespan."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, config_path: Path) -> None:
|
|
27
|
+
self.application: Application = Application(config_path=config_path)
|
|
28
|
+
self.wrapper_entered: bool = False
|
|
29
|
+
self.wrapper_exited: bool = False
|
|
30
|
+
|
|
31
|
+
@asynccontextmanager
|
|
32
|
+
async def lifespan(self) -> AsyncIterator[None]:
|
|
33
|
+
async with self.application.lifespan():
|
|
34
|
+
self.wrapper_entered = True
|
|
35
|
+
try:
|
|
36
|
+
yield
|
|
37
|
+
finally:
|
|
38
|
+
self.wrapper_exited = True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestAnOverrideOwnsTheApplicationAndItsLifespan(TestCase):
|
|
42
|
+
owner: Owner
|
|
43
|
+
|
|
44
|
+
@override
|
|
45
|
+
@classmethod
|
|
46
|
+
@asynccontextmanager
|
|
47
|
+
async def boot_application(cls, config_path: Path) -> AsyncIterator[Application]:
|
|
48
|
+
cls.owner = Owner(config_path)
|
|
49
|
+
async with cls.owner.lifespan():
|
|
50
|
+
yield cls.owner.application
|
|
51
|
+
|
|
52
|
+
async def test_self_app_is_the_application_the_override_yielded(self) -> None:
|
|
53
|
+
assert self.app is type(self).owner.application
|
|
54
|
+
|
|
55
|
+
async def test_the_overrides_own_lifespan_wraps_the_test(self) -> None:
|
|
56
|
+
assert type(self).owner.wrapper_entered
|
|
57
|
+
assert not type(self).owner.wrapper_exited
|
|
58
|
+
|
|
59
|
+
async def test_the_facade_root_points_at_the_yielded_application(self) -> None:
|
|
60
|
+
assert Facade.get_facade_application().unwrap() is self.app
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestFreshApplicationRoutesThroughTheSameHook(FreshApplication):
|
|
64
|
+
"""The per-test opt-out must honour the override too, not bypass it."""
|
|
65
|
+
|
|
66
|
+
owners: ClassVar[list[Owner]] = []
|
|
67
|
+
|
|
68
|
+
@override
|
|
69
|
+
@classmethod
|
|
70
|
+
@asynccontextmanager
|
|
71
|
+
async def boot_application(cls, config_path: Path) -> AsyncIterator[Application]:
|
|
72
|
+
owner = Owner(config_path)
|
|
73
|
+
cls.owners.append(owner)
|
|
74
|
+
async with owner.lifespan():
|
|
75
|
+
yield owner.application
|
|
76
|
+
|
|
77
|
+
async def test_a_builds_through_the_override(self) -> None:
|
|
78
|
+
assert self.app is type(self).owners[-1].application
|
|
79
|
+
|
|
80
|
+
async def test_b_built_a_second_one_and_closed_the_first(self) -> None:
|
|
81
|
+
owners = type(self).owners
|
|
82
|
+
|
|
83
|
+
assert len(owners) == 2, "FreshApplication must re-enter the hook per test"
|
|
84
|
+
assert owners[0].wrapper_exited, "the previous test's lifespan must be closed"
|
|
85
|
+
assert self.app is owners[-1].application
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|