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.
Files changed (184) hide show
  1. {python_neva-5.1.0 → python_neva-5.3.0}/CHANGELOG.md +16 -0
  2. {python_neva-5.1.0 → python_neva-5.3.0}/PKG-INFO +1 -1
  3. {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/service-providers.md +9 -4
  4. {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/testing.md +30 -3
  5. {python_neva-5.1.0 → python_neva-5.3.0}/neva/testing/test_case.py +40 -4
  6. {python_neva-5.1.0 → python_neva-5.3.0}/pyproject.toml +1 -1
  7. python_neva-5.3.0/tests/testing/test_boot_application.py +85 -0
  8. {python_neva-5.1.0 → python_neva-5.3.0}/uv.lock +1 -1
  9. {python_neva-5.1.0 → python_neva-5.3.0}/.claude/settings.local.json +0 -0
  10. {python_neva-5.1.0 → python_neva-5.3.0}/.envrc +0 -0
  11. {python_neva-5.1.0 → python_neva-5.3.0}/.gitignore +0 -0
  12. {python_neva-5.1.0 → python_neva-5.3.0}/.gitlab-ci.yml +0 -0
  13. {python_neva-5.1.0 → python_neva-5.3.0}/.pre-commit-config.yaml +0 -0
  14. {python_neva-5.1.0 → python_neva-5.3.0}/.python-version +0 -0
  15. {python_neva-5.1.0 → python_neva-5.3.0}/CLAUDE.md +0 -0
  16. {python_neva-5.1.0 → python_neva-5.3.0}/README.md +0 -0
  17. {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/__init__.py +0 -0
  18. {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/application.py +0 -0
  19. {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/config.py +0 -0
  20. {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/facade.py +0 -0
  21. {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/integrations/__init__.py +0 -0
  22. {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/integrations/faststream.py +0 -0
  23. {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/markers.py +0 -0
  24. {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/py.typed +0 -0
  25. {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/scopes.py +0 -0
  26. {python_neva-5.1.0 → python_neva-5.3.0}/neva/arch/service_provider.py +0 -0
  27. {python_neva-5.1.0 → python_neva-5.3.0}/neva/config/__init__.py +0 -0
  28. {python_neva-5.1.0 → python_neva-5.3.0}/neva/config/base_providers.py +0 -0
  29. {python_neva-5.1.0 → python_neva-5.3.0}/neva/config/loader.py +0 -0
  30. {python_neva-5.1.0 → python_neva-5.3.0}/neva/config/py.typed +0 -0
  31. {python_neva-5.1.0 → python_neva-5.3.0}/neva/config/repository.py +0 -0
  32. {python_neva-5.1.0 → python_neva-5.3.0}/neva/database/__init__.py +0 -0
  33. {python_neva-5.1.0 → python_neva-5.3.0}/neva/database/config.py +0 -0
  34. {python_neva-5.1.0 → python_neva-5.3.0}/neva/database/connection.py +0 -0
  35. {python_neva-5.1.0 → python_neva-5.3.0}/neva/database/manager.py +0 -0
  36. {python_neva-5.1.0 → python_neva-5.3.0}/neva/database/provider.py +0 -0
  37. {python_neva-5.1.0 → python_neva-5.3.0}/neva/database/py.typed +0 -0
  38. {python_neva-5.1.0 → python_neva-5.3.0}/neva/database/transaction.py +0 -0
  39. {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/__init__.py +0 -0
  40. {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/contracts/__init__.py +0 -0
  41. {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/contracts/dispatcher.py +0 -0
  42. {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/contracts/event.py +0 -0
  43. {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/contracts/handler.py +0 -0
  44. {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/contracts/listener.py +0 -0
  45. {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/dispatcher.py +0 -0
  46. {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/event.py +0 -0
  47. {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/event_registry.py +0 -0
  48. {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/listener.py +0 -0
  49. {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/policy.py +0 -0
  50. {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/provider.py +0 -0
  51. {python_neva-5.1.0 → python_neva-5.3.0}/neva/events/py.typed +0 -0
  52. {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/__init__.py +0 -0
  53. {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/configuration.md +0 -0
  54. {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/database-transactions.md +0 -0
  55. {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/events.md +0 -0
  56. {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/facades.md +0 -0
  57. {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/factories.md +0 -0
  58. {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/observability.md +0 -0
  59. {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/result-option.md +0 -0
  60. {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/fragments/security.md +0 -0
  61. {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/manifest.py +0 -0
  62. {python_neva-5.1.0 → python_neva-5.3.0}/neva/guidelines/py.typed +0 -0
  63. {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/__init__.py +0 -0
  64. {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/config.py +0 -0
  65. {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/logging/__init__.py +0 -0
  66. {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/logging/channels.py +0 -0
  67. {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/logging/contracts.py +0 -0
  68. {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/logging/manager.py +0 -0
  69. {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/logging/provider.py +0 -0
  70. {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/logging/resolver.py +0 -0
  71. {python_neva-5.1.0 → python_neva-5.3.0}/neva/obs/py.typed +0 -0
  72. {python_neva-5.1.0 → python_neva-5.3.0}/neva/polyfactory/__init__.py +0 -0
  73. {python_neva-5.1.0 → python_neva-5.3.0}/neva/polyfactory/factories.py +0 -0
  74. {python_neva-5.1.0 → python_neva-5.3.0}/neva/polyfactory/persistence.py +0 -0
  75. {python_neva-5.1.0 → python_neva-5.3.0}/neva/polyfactory/py.typed +0 -0
  76. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/__init__.py +0 -0
  77. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/encryption/__init__.py +0 -0
  78. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/encryption/encrypter.py +0 -0
  79. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/encryption/protocol.py +0 -0
  80. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/hashing/__init__.py +0 -0
  81. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/hashing/config.py +0 -0
  82. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/hashing/hash_manager.py +0 -0
  83. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/hashing/hashers/__init__.py +0 -0
  84. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/hashing/hashers/argon2.py +0 -0
  85. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
  86. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/hashing/hashers/protocol.py +0 -0
  87. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/provider.py +0 -0
  88. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/py.typed +0 -0
  89. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/tokens/__init__.py +0 -0
  90. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/tokens/generate_token.py +0 -0
  91. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/tokens/hash_token.py +0 -0
  92. {python_neva-5.1.0 → python_neva-5.3.0}/neva/security/tokens/verify_token.py +0 -0
  93. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/__init__.py +0 -0
  94. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/accessors.py +0 -0
  95. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/__init__.py +0 -0
  96. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/app.py +0 -0
  97. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/app.pyi +0 -0
  98. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/config.py +0 -0
  99. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/config.pyi +0 -0
  100. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/crypt.py +0 -0
  101. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/crypt.pyi +0 -0
  102. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/db.py +0 -0
  103. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/db.pyi +0 -0
  104. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/event.py +0 -0
  105. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/event.pyi +0 -0
  106. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/hash.py +0 -0
  107. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/hash.pyi +0 -0
  108. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/log.py +0 -0
  109. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/facade/log.pyi +0 -0
  110. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/py.typed +0 -0
  111. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/results.py +0 -0
  112. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/strategy.py +0 -0
  113. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/strconv.py +0 -0
  114. {python_neva-5.1.0 → python_neva-5.3.0}/neva/support/time.py +0 -0
  115. {python_neva-5.1.0 → python_neva-5.3.0}/neva/testing/__init__.py +0 -0
  116. {python_neva-5.1.0 → python_neva-5.3.0}/neva/testing/fakes.py +0 -0
  117. {python_neva-5.1.0 → python_neva-5.3.0}/neva/testing/fixtures.py +0 -0
  118. {python_neva-5.1.0 → python_neva-5.3.0}/neva/testing/py.typed +0 -0
  119. {python_neva-5.1.0 → python_neva-5.3.0}/ruff.toml +0 -0
  120. {python_neva-5.1.0 → python_neva-5.3.0}/scripts/retag-with-changelog.sh +0 -0
  121. {python_neva-5.1.0 → python_neva-5.3.0}/tests/__init__.py +0 -0
  122. {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/__init__.py +0 -0
  123. {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_cache.py +0 -0
  124. {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_config_shapes.py +0 -0
  125. {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_context.py +0 -0
  126. {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_extends.py +0 -0
  127. {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_facade_resolution.py +0 -0
  128. {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_facade_root_nesting.py +0 -0
  129. {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_lifetimes.py +0 -0
  130. {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_register_resolution.py +0 -0
  131. {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_registration.py +0 -0
  132. {python_neva-5.1.0 → python_neva-5.3.0}/tests/arch/test_scope.py +0 -0
  133. {python_neva-5.1.0 → python_neva-5.3.0}/tests/config/__init__.py +0 -0
  134. {python_neva-5.1.0 → python_neva-5.3.0}/tests/config/test_config_path_resolution.py +0 -0
  135. {python_neva-5.1.0 → python_neva-5.3.0}/tests/config/test_loader.py +0 -0
  136. {python_neva-5.1.0 → python_neva-5.3.0}/tests/config/test_repository.py +0 -0
  137. {python_neva-5.1.0 → python_neva-5.3.0}/tests/conftest.py +0 -0
  138. {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/__init__.py +0 -0
  139. {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_connection_manager.py +0 -0
  140. {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_database_manager.py +0 -0
  141. {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_detached_lifespan.py +0 -0
  142. {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_edge_cases.py +0 -0
  143. {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_multi_connection.py +0 -0
  144. {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_sqlalchemy_integration.py +0 -0
  145. {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_transaction.py +0 -0
  146. {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_transaction_callbacks.py +0 -0
  147. {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_transaction_context.py +0 -0
  148. {python_neva-5.1.0 → python_neva-5.3.0}/tests/database/test_transaction_registry.py +0 -0
  149. {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/__init__.py +0 -0
  150. {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/conftest.py +0 -0
  151. {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_before_dispatch.py +0 -0
  152. {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_binding.py +0 -0
  153. {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_deferred.py +0 -0
  154. {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_dispatch.py +0 -0
  155. {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_event.py +0 -0
  156. {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_function_listener.py +0 -0
  157. {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_immediate.py +0 -0
  158. {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_listen_on_parent_class.py +0 -0
  159. {python_neva-5.1.0 → python_neva-5.3.0}/tests/events/test_listener_wiring.py +0 -0
  160. {python_neva-5.1.0 → python_neva-5.3.0}/tests/obs/__init__.py +0 -0
  161. {python_neva-5.1.0 → python_neva-5.3.0}/tests/obs/conftest.py +0 -0
  162. {python_neva-5.1.0 → python_neva-5.3.0}/tests/obs/test_channels.py +0 -0
  163. {python_neva-5.1.0 → python_neva-5.3.0}/tests/obs/test_facade.py +0 -0
  164. {python_neva-5.1.0 → python_neva-5.3.0}/tests/obs/test_manager.py +0 -0
  165. {python_neva-5.1.0 → python_neva-5.3.0}/tests/obs/test_provider_hook.py +0 -0
  166. {python_neva-5.1.0 → python_neva-5.3.0}/tests/obs/test_resolver.py +0 -0
  167. {python_neva-5.1.0 → python_neva-5.3.0}/tests/polyfactory/__init__.py +0 -0
  168. {python_neva-5.1.0 → python_neva-5.3.0}/tests/polyfactory/test_model_factory.py +0 -0
  169. {python_neva-5.1.0 → python_neva-5.3.0}/tests/security/__init__.py +0 -0
  170. {python_neva-5.1.0 → python_neva-5.3.0}/tests/security/test_config_shapes.py +0 -0
  171. {python_neva-5.1.0 → python_neva-5.3.0}/tests/security/test_encrypter.py +0 -0
  172. {python_neva-5.1.0 → python_neva-5.3.0}/tests/security/test_hash_manager.py +0 -0
  173. {python_neva-5.1.0 → python_neva-5.3.0}/tests/security/test_tokens.py +0 -0
  174. {python_neva-5.1.0 → python_neva-5.3.0}/tests/support/__init__.py +0 -0
  175. {python_neva-5.1.0 → python_neva-5.3.0}/tests/support/test_results.py +0 -0
  176. {python_neva-5.1.0 → python_neva-5.3.0}/tests/support/test_strategy.py +0 -0
  177. {python_neva-5.1.0 → python_neva-5.3.0}/tests/testing/__init__.py +0 -0
  178. {python_neva-5.1.0 → python_neva-5.3.0}/tests/testing/test_application_reuse.py +0 -0
  179. {python_neva-5.1.0 → python_neva-5.3.0}/tests/testing/test_create_config_migration.py +0 -0
  180. {python_neva-5.1.0 → python_neva-5.3.0}/tests/testing/test_event_fake.py +0 -0
  181. {python_neva-5.1.0 → python_neva-5.3.0}/tests/testing/test_facade_restore.py +0 -0
  182. {python_neva-5.1.0 → python_neva-5.3.0}/tests/testing/test_fixtures.py +0 -0
  183. {python_neva-5.1.0 → python_neva-5.3.0}/tests/testing/test_refresh_database.py +0 -0
  184. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-neva
3
- Version: 5.1.0
3
+ Version: 5.3.0
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: aiosqlite>=0.20.0
@@ -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()` is idempotent for an already-registered class, but **returns `Err` once the
156
- application has booted**: dishka validates the graph at build time, and the booted container
157
- holds APP-scoped instances a rebuild would orphan. Declare providers in config, or register
158
- before entering `lifespan()`.
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>=4.0
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
- app = Application(config_path=_test_case_config)
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
- app = Application(config_path=self._build_config(tmp_path))
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-neva"
3
- version = "5.1.0"
3
+ version = "5.3.0"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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
@@ -1539,7 +1539,7 @@ wheels = [
1539
1539
 
1540
1540
  [[package]]
1541
1541
  name = "python-neva"
1542
- version = "5.1.0"
1542
+ version = "5.3.0"
1543
1543
  source = { editable = "." }
1544
1544
  dependencies = [
1545
1545
  { name = "aiosqlite" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes