wireup 2.4.0__tar.gz → 2.5.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 (218) hide show
  1. {wireup-2.4.0 → wireup-2.5.0}/.vscode/settings.json +1 -0
  2. {wireup-2.4.0 → wireup-2.5.0}/PKG-INFO +4 -22
  3. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/index.md +2 -1
  4. {wireup-2.4.0 → wireup-2.5.0}/pyproject.toml +1 -1
  5. {wireup-2.4.0 → wireup-2.5.0}/readme.md +3 -21
  6. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_as_type.py +77 -0
  7. wireup-2.5.0/test/unit/test_concurrency.py +85 -0
  8. wireup-2.4.0/z_test_codegen.py → wireup-2.5.0/wireup/codegen.py +2 -0
  9. {wireup-2.4.0 → wireup-2.5.0}/wireup/integration/aiohttp.py +4 -4
  10. {wireup-2.4.0 → wireup-2.5.0}/wireup/integration/fastapi.py +1 -2
  11. {wireup-2.4.0 → wireup-2.5.0}/wireup/ioc/container/__init__.py +1 -1
  12. {wireup-2.4.0 → wireup-2.5.0}/wireup/ioc/container/base_container.py +9 -0
  13. wireup-2.5.0/wireup/ioc/container/lock_registry.py +20 -0
  14. wireup-2.5.0/wireup/ioc/factory_compiler.py +245 -0
  15. wireup-2.5.0/z_test_bench.py +103 -0
  16. wireup-2.5.0/z_test_repro_fastapi_race.py +123 -0
  17. wireup-2.5.0/z_test_repro_wireup_race.py +120 -0
  18. wireup-2.4.0/wireup/ioc/factory_compiler.py +0 -175
  19. wireup-2.4.0/z_test_CHEATSHEET.md +0 -476
  20. wireup-2.4.0/z_test_abstract.py +0 -19
  21. wireup-2.4.0/z_test_benchmark.py +0 -126
  22. wireup-2.4.0/z_test_binds.py +0 -92
  23. wireup-2.4.0/z_test_blog_post.md +0 -169
  24. wireup-2.4.0/z_test_circular.py +0 -26
  25. wireup-2.4.0/z_test_err.md +0 -56
  26. wireup-2.4.0/z_test_fastapi-zero-overhead.md +0 -181
  27. wireup-2.4.0/z_test_get.py +0 -39
  28. wireup-2.4.0/z_test_inheritance.py +0 -42
  29. wireup-2.4.0/z_test_instance.py +0 -17
  30. wireup-2.4.0/z_test_interfaces.py +0 -52
  31. wireup-2.4.0/z_test_isolated.py +0 -31
  32. wireup-2.4.0/z_test_mapping.py +0 -13
  33. wireup-2.4.0/z_test_mermaid.py +0 -103
  34. wireup-2.4.0/z_test_new.py +0 -20
  35. wireup-2.4.0/z_test_optional.py +0 -57
  36. wireup-2.4.0/z_test_override.py +0 -27
  37. wireup-2.4.0/z_test_param.py +0 -7
  38. wireup-2.4.0/z_test_param_output.txt +0 -4
  39. wireup-2.4.0/z_test_param_output_2.txt +0 -20
  40. wireup-2.4.0/z_test_param_output_3.txt +0 -4
  41. wireup-2.4.0/z_test_protocol_mypy.py +0 -33
  42. wireup-2.4.0/z_test_py_ver.py +0 -3
  43. wireup-2.4.0/z_test_pydantic.py +0 -31
  44. wireup-2.4.0/z_test_repo_summary.md +0 -162
  45. wireup-2.4.0/z_test_repro.py +0 -51
  46. wireup-2.4.0/z_test_repro_results_2.txt +0 -5
  47. wireup-2.4.0/z_test_roadmap.md +0 -2
  48. wireup-2.4.0/z_test_runtime_checkable.py +0 -14
  49. wireup-2.4.0/z_test_thing.py +0 -12
  50. {wireup-2.4.0 → wireup-2.5.0}/.agent/rules.md +0 -0
  51. {wireup-2.4.0 → wireup-2.5.0}/.agent/workflows/context.md +0 -0
  52. {wireup-2.4.0 → wireup-2.5.0}/.github/contributing.md +0 -0
  53. {wireup-2.4.0 → wireup-2.5.0}/.github/workflows/run_all.yml +0 -0
  54. {wireup-2.4.0 → wireup-2.5.0}/.github/workflows/run_integration_tests.yml +0 -0
  55. {wireup-2.4.0 → wireup-2.5.0}/.gitignore +0 -0
  56. {wireup-2.4.0 → wireup-2.5.0}/.vscode/launch.json +0 -0
  57. {wireup-2.4.0 → wireup-2.5.0}/Makefile +0 -0
  58. {wireup-2.4.0 → wireup-2.5.0}/docs/mkdocs.yml +0 -0
  59. {wireup-2.4.0 → wireup-2.5.0}/docs/overrides/main.html +0 -0
  60. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/class/aiohttp_integration.md +0 -0
  61. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/class/click_integration.md +0 -0
  62. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/class/config_store.md +0 -0
  63. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/class/django_integration.md +0 -0
  64. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/class/fastapi_integration.md +0 -0
  65. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/class/flask_integration.md +0 -0
  66. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/class/override_manager.md +0 -0
  67. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/class/starlette_integration.md +0 -0
  68. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/class/wireup.md +0 -0
  69. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/configuration.md +0 -0
  70. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/container.md +0 -0
  71. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/factories.md +0 -0
  72. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/function_injection.md +0 -0
  73. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/getting_started.md +0 -0
  74. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/injectables.md +0 -0
  75. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/integrations/aiohttp/class_based_handlers.md +0 -0
  76. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/integrations/aiohttp/index.md +0 -0
  77. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/integrations/click/index.md +0 -0
  78. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/integrations/django/index.md +0 -0
  79. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/integrations/fastapi/class_based_handlers.md +0 -0
  80. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/integrations/fastapi/direct_container_access.md +0 -0
  81. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/integrations/fastapi/index.md +0 -0
  82. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/integrations/flask/index.md +0 -0
  83. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/integrations/index.md +0 -0
  84. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/integrations/starlette/index.md +0 -0
  85. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/interfaces.md +0 -0
  86. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/lifetimes_and_scopes.md +0 -0
  87. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/resources.md +0 -0
  88. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/stylesheets/extra.css +0 -0
  89. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/testing.md +0 -0
  90. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/upgrading.md +0 -0
  91. {wireup-2.4.0 → wireup-2.5.0}/docs/pages/versioning.md +0 -0
  92. {wireup-2.4.0 → wireup-2.5.0}/license.md +0 -0
  93. {wireup-2.4.0 → wireup-2.5.0}/profile_tests.py +0 -0
  94. {wireup-2.4.0 → wireup-2.5.0}/test/__init__.py +0 -0
  95. {wireup-2.4.0 → wireup-2.5.0}/test/conftest.py +0 -0
  96. {wireup-2.4.0 → wireup-2.5.0}/test/integration/__init__.py +0 -0
  97. {wireup-2.4.0 → wireup-2.5.0}/test/integration/aiohttp/__init__.py +0 -0
  98. {wireup-2.4.0 → wireup-2.5.0}/test/integration/aiohttp/handler.py +0 -0
  99. {wireup-2.4.0 → wireup-2.5.0}/test/integration/aiohttp/routes.py +0 -0
  100. {wireup-2.4.0 → wireup-2.5.0}/test/integration/aiohttp/services.py +0 -0
  101. {wireup-2.4.0 → wireup-2.5.0}/test/integration/aiohttp/test_aiohttp_integration.py +0 -0
  102. {wireup-2.4.0 → wireup-2.5.0}/test/integration/click/__init__.py +0 -0
  103. {wireup-2.4.0 → wireup-2.5.0}/test/integration/click/test_click_integration.py +0 -0
  104. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/__init__.py +0 -0
  105. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/__init__.py +0 -0
  106. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/app_1/__init__.py +0 -0
  107. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/app_1/app.py +0 -0
  108. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/app_1/urls.py +0 -0
  109. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/app_1/views.py +0 -0
  110. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/app_2/__init__.py +0 -0
  111. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/app_2/app.py +0 -0
  112. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/app_2/urls.py +0 -0
  113. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/app_2/views.py +0 -0
  114. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/drf_app/__init__.py +0 -0
  115. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/drf_app/app.py +0 -0
  116. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/drf_app/urls.py +0 -0
  117. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/drf_app/views.py +0 -0
  118. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/ninja_app/__init__.py +0 -0
  119. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/ninja_app/app.py +0 -0
  120. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/ninja_app/urls.py +0 -0
  121. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/apps/ninja_app/views.py +0 -0
  122. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/factory/__init__.py +0 -0
  123. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/factory/factories.py +0 -0
  124. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/injectable/__init__.py +0 -0
  125. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/injectable/current_request_service.py +0 -0
  126. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/injectable/random_service.py +0 -0
  127. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/templates/bar.html +0 -0
  128. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/templates/foo.html +0 -0
  129. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/test_django_integration.py +0 -0
  130. {wireup-2.4.0 → wireup-2.5.0}/test/integration/django/view.py +0 -0
  131. {wireup-2.4.0 → wireup-2.5.0}/test/integration/fastapi/__init__.py +0 -0
  132. {wireup-2.4.0 → wireup-2.5.0}/test/integration/fastapi/cbr.py +0 -0
  133. {wireup-2.4.0 → wireup-2.5.0}/test/integration/fastapi/router.py +0 -0
  134. {wireup-2.4.0 → wireup-2.5.0}/test/integration/fastapi/services.py +0 -0
  135. {wireup-2.4.0 → wireup-2.5.0}/test/integration/fastapi/test_fastapi_integration.py +0 -0
  136. {wireup-2.4.0 → wireup-2.5.0}/test/integration/fastapi/wireup_route.py +0 -0
  137. {wireup-2.4.0 → wireup-2.5.0}/test/integration/flask/__init__.py +0 -0
  138. {wireup-2.4.0 → wireup-2.5.0}/test/integration/flask/bp.py +0 -0
  139. {wireup-2.4.0 → wireup-2.5.0}/test/integration/flask/services/__init__.py +0 -0
  140. {wireup-2.4.0 → wireup-2.5.0}/test/integration/flask/services/factories.py +0 -0
  141. {wireup-2.4.0 → wireup-2.5.0}/test/integration/flask/services/is_test_service.py +0 -0
  142. {wireup-2.4.0 → wireup-2.5.0}/test/integration/flask/test_flask_integration.py +0 -0
  143. {wireup-2.4.0 → wireup-2.5.0}/test/integration/starlette/__init__.py +0 -0
  144. {wireup-2.4.0 → wireup-2.5.0}/test/integration/starlette/test_starlette_integration.py +0 -0
  145. {wireup-2.4.0 → wireup-2.5.0}/test/shared/__init__.py +0 -0
  146. {wireup-2.4.0 → wireup-2.5.0}/test/shared/shared_services/__init__.py +0 -0
  147. {wireup-2.4.0 → wireup-2.5.0}/test/shared/shared_services/greeter.py +0 -0
  148. {wireup-2.4.0 → wireup-2.5.0}/test/shared/shared_services/rand.py +0 -0
  149. {wireup-2.4.0 → wireup-2.5.0}/test/shared/shared_services/scoped.py +0 -0
  150. {wireup-2.4.0 → wireup-2.5.0}/test/unit/__init__.py +0 -0
  151. {wireup-2.4.0 → wireup-2.5.0}/test/unit/service_refs/__init__.py +0 -0
  152. {wireup-2.4.0 → wireup-2.5.0}/test/unit/services/__init__.py +0 -0
  153. {wireup-2.4.0 → wireup-2.5.0}/test/unit/services/abstract_multiple_bases.py +0 -0
  154. {wireup-2.4.0 → wireup-2.5.0}/test/unit/services/inheritance_test/__init__.py +0 -0
  155. {wireup-2.4.0 → wireup-2.5.0}/test/unit/services/inheritance_test/base.py +0 -0
  156. {wireup-2.4.0 → wireup-2.5.0}/test/unit/services/no_annotations/__init__.py +0 -0
  157. {wireup-2.4.0 → wireup-2.5.0}/test/unit/services/no_annotations/random/__init__.py +0 -0
  158. {wireup-2.4.0 → wireup-2.5.0}/test/unit/services/no_annotations/random/random_service.py +0 -0
  159. {wireup-2.4.0 → wireup-2.5.0}/test/unit/services/no_annotations/random/truly_random_service.py +0 -0
  160. {wireup-2.4.0 → wireup-2.5.0}/test/unit/services/with_annotations/__init__.py +0 -0
  161. {wireup-2.4.0 → wireup-2.5.0}/test/unit/services/with_annotations/env/__init__.py +0 -0
  162. {wireup-2.4.0 → wireup-2.5.0}/test/unit/services/with_annotations/services.py +0 -0
  163. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_container_core.py +0 -0
  164. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_container_creation.py +0 -0
  165. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_container_generator_factory.py +0 -0
  166. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_container_override.py +0 -0
  167. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_container_scope.py +0 -0
  168. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_eval_type.py +0 -0
  169. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_exit_stack.py +0 -0
  170. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_inject_from_container.py +0 -0
  171. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_inject_from_container_optional_types.py +0 -0
  172. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_inject_from_container_stringified_types.py +0 -0
  173. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_module_loading.py +0 -0
  174. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_optional_type.py +0 -0
  175. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_parameter.py +0 -0
  176. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_service_deprecation.py +0 -0
  177. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_service_registry.py +0 -0
  178. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_signature_hiding.py +0 -0
  179. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_type_analysis.py +0 -0
  180. {wireup-2.4.0 → wireup-2.5.0}/test/unit/test_util.py +0 -0
  181. {wireup-2.4.0 → wireup-2.5.0}/test/unit/util.py +0 -0
  182. {wireup-2.4.0 → wireup-2.5.0}/tox.ini +0 -0
  183. {wireup-2.4.0 → wireup-2.5.0}/uv.lock +0 -0
  184. {wireup-2.4.0 → wireup-2.5.0}/wireup/__init__.py +0 -0
  185. {wireup-2.4.0 → wireup-2.5.0}/wireup/_annotations.py +0 -0
  186. {wireup-2.4.0 → wireup-2.5.0}/wireup/_decorators.py +0 -0
  187. {wireup-2.4.0 → wireup-2.5.0}/wireup/_discovery.py +0 -0
  188. {wireup-2.4.0 → wireup-2.5.0}/wireup/errors.py +0 -0
  189. {wireup-2.4.0 → wireup-2.5.0}/wireup/integration/__init__.py +0 -0
  190. {wireup-2.4.0 → wireup-2.5.0}/wireup/integration/click.py +0 -0
  191. {wireup-2.4.0 → wireup-2.5.0}/wireup/integration/django/__init__.py +0 -0
  192. {wireup-2.4.0 → wireup-2.5.0}/wireup/integration/django/apps.py +0 -0
  193. {wireup-2.4.0 → wireup-2.5.0}/wireup/integration/django/decorators.py +0 -0
  194. {wireup-2.4.0 → wireup-2.5.0}/wireup/integration/flask.py +0 -0
  195. {wireup-2.4.0 → wireup-2.5.0}/wireup/integration/starlette.py +0 -0
  196. {wireup-2.4.0 → wireup-2.5.0}/wireup/ioc/__init__.py +0 -0
  197. {wireup-2.4.0 → wireup-2.5.0}/wireup/ioc/_exit_stack.py +0 -0
  198. {wireup-2.4.0 → wireup-2.5.0}/wireup/ioc/configuration.py +0 -0
  199. {wireup-2.4.0 → wireup-2.5.0}/wireup/ioc/container/async_container.py +0 -0
  200. {wireup-2.4.0 → wireup-2.5.0}/wireup/ioc/container/sync_container.py +0 -0
  201. {wireup-2.4.0 → wireup-2.5.0}/wireup/ioc/override_manager.py +0 -0
  202. {wireup-2.4.0 → wireup-2.5.0}/wireup/ioc/registry.py +0 -0
  203. {wireup-2.4.0 → wireup-2.5.0}/wireup/ioc/type_analysis.py +0 -0
  204. {wireup-2.4.0 → wireup-2.5.0}/wireup/ioc/types.py +0 -0
  205. {wireup-2.4.0 → wireup-2.5.0}/wireup/ioc/util.py +0 -0
  206. {wireup-2.4.0 → wireup-2.5.0}/wireup/py.typed +0 -0
  207. {wireup-2.4.0 → wireup-2.5.0}/wireup/util.py +0 -0
  208. {wireup-2.4.0 → wireup-2.5.0}/z_test_prep/__init__.py +0 -0
  209. {wireup-2.4.0 → wireup-2.5.0}/z_test_prep/js_stack.py +0 -0
  210. {wireup-2.4.0 → wireup-2.5.0}/z_test_prep/z_test_cit_code1.py +0 -0
  211. {wireup-2.4.0 → wireup-2.5.0}/z_test_prep/z_test_cit_code2.py +0 -0
  212. {wireup-2.4.0 → wireup-2.5.0}/z_test_prep/z_test_cit_palindrome.py +0 -0
  213. {wireup-2.4.0 → wireup-2.5.0}/z_test_prep/z_test_citadel_oa1.py +0 -0
  214. {wireup-2.4.0 → wireup-2.5.0}/z_test_prep/z_test_js_code1.py +0 -0
  215. {wireup-2.4.0 → wireup-2.5.0}/z_test_prep/z_test_js_code2.py +0 -0
  216. {wireup-2.4.0 → wireup-2.5.0}/z_test_services/__init__.py +0 -0
  217. {wireup-2.4.0 → wireup-2.5.0}/z_test_services/bar.py +0 -0
  218. {wireup-2.4.0 → wireup-2.5.0}/z_test_services/foo.py +0 -0
@@ -12,6 +12,7 @@
12
12
  "cursorpyright.analysis.autoImportCompletions": true,
13
13
  "cSpell.words": [
14
14
  "asyncio",
15
+ "Codegen",
15
16
  "pyrefly"
16
17
  ],
17
18
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wireup
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: Python Dependency Injection Library
5
5
  Project-URL: Homepage, https://github.com/maldoinc/wireup
6
6
  Project-URL: Repository, https://github.com/maldoinc/wireup
@@ -47,7 +47,9 @@ Description-Content-Type: text/markdown
47
47
  [![Documentation](https://img.shields.io/badge/%F0%9F%93%9A%20Documentation-3D9970)](https://maldoinc.github.io/wireup)
48
48
  </div>
49
49
 
50
- Automate dependency management using Python's type system. Build complex applications with native support for async and generators, plus integrations for popular frameworks out of the box.
50
+ Automate dependency management using Python's type system. Build complex applications with native support for async and
51
+ generators, plus integrations for popular frameworks out of the box. Wireup is thread-safe and ready for no-GIL
52
+ Python (PEP 703).
51
53
 
52
54
  > [!TIP]
53
55
  > **New**: Inject Dependencies in FastAPI with zero runtime overhead using [Class-Based Handlers](https://maldoinc.github.io/wireup/latest/integrations/fastapi/class_based_handlers/).
@@ -339,26 +341,6 @@ def command(service: Injected[UserService]): ...
339
341
  def task(service: Injected[UserService]): ...
340
342
  ```
341
343
 
342
- ```mermaid
343
- graph LR
344
- subgraph "Core Business Logic"
345
- Services[Injectables & Services]
346
- Domain[Domain Models]
347
- end
348
-
349
- subgraph "Entry Points"
350
- API[FastAPI / Web]
351
- CLI[Click / CLI]
352
- Worker[Celery / Worker]
353
- end
354
-
355
- API -->|Uses| Services
356
- CLI -->|Uses| Services
357
- Worker -->|Uses| Services
358
-
359
- Services --> Domain
360
- ```
361
-
362
344
  ### 🔌 Native Integration with popular frameworks
363
345
 
364
346
  Integrate with popular frameworks for a smoother developer experience.
@@ -5,7 +5,8 @@ Performant, concise and type-safe Dependency Injection for Python.
5
5
  [![PyPI - Version](https://img.shields.io/pypi/v/wireup)](https://pypi.org/project/wireup/)
6
6
 
7
7
  Automate dependency management using Python's type system. Build complex applications with native support for async and
8
- generators, plus integrations for popular frameworks out of the box.
8
+ generators, plus integrations for popular frameworks out of the box. Wireup is thread-safe and ready for no-GIL Python
9
+ (PEP 703).
9
10
 
10
11
  !!! tip "Zero Runtime Overhead"
11
12
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "wireup"
3
- version = "2.4.0"
3
+ version = "2.5.0"
4
4
  description = "Python Dependency Injection Library"
5
5
  authors = [{ name = "Aldo Mateli", email = "aldo.mateli@gmail.com" }]
6
6
  license = { text = "MIT" }
@@ -9,7 +9,9 @@
9
9
  [![Documentation](https://img.shields.io/badge/%F0%9F%93%9A%20Documentation-3D9970)](https://maldoinc.github.io/wireup)
10
10
  </div>
11
11
 
12
- Automate dependency management using Python's type system. Build complex applications with native support for async and generators, plus integrations for popular frameworks out of the box.
12
+ Automate dependency management using Python's type system. Build complex applications with native support for async and
13
+ generators, plus integrations for popular frameworks out of the box. Wireup is thread-safe and ready for no-GIL
14
+ Python (PEP 703).
13
15
 
14
16
  > [!TIP]
15
17
  > **New**: Inject Dependencies in FastAPI with zero runtime overhead using [Class-Based Handlers](https://maldoinc.github.io/wireup/latest/integrations/fastapi/class_based_handlers/).
@@ -301,26 +303,6 @@ def command(service: Injected[UserService]): ...
301
303
  def task(service: Injected[UserService]): ...
302
304
  ```
303
305
 
304
- ```mermaid
305
- graph LR
306
- subgraph "Core Business Logic"
307
- Services[Injectables & Services]
308
- Domain[Domain Models]
309
- end
310
-
311
- subgraph "Entry Points"
312
- API[FastAPI / Web]
313
- CLI[Click / CLI]
314
- Worker[Celery / Worker]
315
- end
316
-
317
- API -->|Uses| Services
318
- CLI -->|Uses| Services
319
- Worker -->|Uses| Services
320
-
321
- Services --> Domain
322
- ```
323
-
324
306
  ### 🔌 Native Integration with popular frameworks
325
307
 
326
308
  Integrate with popular frameworks for a smoother developer experience.
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import sys
4
+ from abc import ABC, abstractmethod
5
+ from dataclasses import dataclass
4
6
  from typing import Optional, Protocol
5
7
 
6
8
  import pytest
@@ -171,3 +173,78 @@ def test_as_type_on_optional_factory_new_union_optional():
171
173
  instance = container.get(OptionalProto | None)
172
174
  assert isinstance(instance, OptionalImpl)
173
175
  assert instance.opt() == "opt"
176
+
177
+
178
+ class AbstractFoo(ABC):
179
+ @abstractmethod
180
+ def bar(self) -> str: ...
181
+
182
+
183
+ @injectable(as_type=AbstractFoo)
184
+ class AbstractFooImpl(AbstractFoo):
185
+ def bar(self) -> str:
186
+ return "bar"
187
+
188
+
189
+ def test_register_with_as_type_abc():
190
+ container = create_sync_container(injectables=[AbstractFooImpl])
191
+
192
+ # Should be resolvable via the abstraction
193
+ instance = container.get(AbstractFoo)
194
+ assert isinstance(instance, AbstractFooImpl)
195
+ assert instance.bar() == "bar"
196
+
197
+ # Should NOT be resolvable via the implementation class directly
198
+ with pytest.raises(UnknownServiceRequestedError):
199
+ container.get(AbstractFooImpl)
200
+
201
+
202
+ class FunctionProto(Protocol):
203
+ def call(self) -> int: ...
204
+
205
+
206
+ class RealService:
207
+ def call(self) -> int:
208
+ return 42
209
+
210
+
211
+ def test_as_type_on_simple_function():
212
+ @injectable(as_type=FunctionProto)
213
+ def factory() -> RealService:
214
+ return RealService()
215
+
216
+ container = create_sync_container(injectables=[factory])
217
+
218
+ svc = container.get(FunctionProto)
219
+ assert svc.call() == 42
220
+ assert isinstance(svc, RealService)
221
+
222
+
223
+ class DependencyProto(Protocol):
224
+ def do_something(self) -> str: ...
225
+
226
+
227
+ class DependencyImpl:
228
+ def do_something(self) -> str:
229
+ return "dependency_impl"
230
+
231
+
232
+ @injectable
233
+ @dataclass
234
+ class ServiceWithDependency:
235
+ dependency: DependencyProto
236
+
237
+
238
+ def test_as_type_factory_dependency_injection():
239
+ # Test that a dependency produced by a factory with as_type
240
+ # is correctly injected into another service.
241
+
242
+ @injectable(as_type=DependencyProto)
243
+ def make_dependency() -> DependencyImpl:
244
+ return DependencyImpl()
245
+
246
+ container = create_sync_container(injectables=[make_dependency, ServiceWithDependency])
247
+
248
+ service = container.get(ServiceWithDependency)
249
+ assert isinstance(service.dependency, DependencyImpl)
250
+ assert service.dependency.do_something() == "dependency_impl"
@@ -0,0 +1,85 @@
1
+ import asyncio
2
+
3
+ import pytest
4
+ from wireup import create_async_container, injectable
5
+
6
+
7
+ class Counter:
8
+ pass
9
+
10
+
11
+ class ScopedCounter:
12
+ pass
13
+
14
+
15
+ @injectable
16
+ async def counter_factory() -> Counter:
17
+ await asyncio.sleep(0.01)
18
+ return Counter()
19
+
20
+
21
+ @injectable(lifetime="scoped")
22
+ async def scoped_counter_factory() -> ScopedCounter:
23
+ await asyncio.sleep(0.01)
24
+ return ScopedCounter()
25
+
26
+
27
+ async def test_singleton_concurrency() -> None:
28
+ container = create_async_container(services=[counter_factory])
29
+
30
+ results = await asyncio.gather(
31
+ container.get(Counter),
32
+ container.get(Counter),
33
+ container.get(Counter),
34
+ container.get(Counter),
35
+ container.get(Counter),
36
+ )
37
+
38
+ unique_instances = set(results)
39
+ assert len(unique_instances) == 1, f"Expected 1 singleton instance, got {len(unique_instances)}"
40
+
41
+
42
+ async def test_scoped_concurrency_same_scope() -> None:
43
+ container = create_async_container(services=[scoped_counter_factory])
44
+
45
+ async with container.enter_scope() as scope:
46
+ results = await asyncio.gather(
47
+ scope.get(ScopedCounter),
48
+ scope.get(ScopedCounter),
49
+ scope.get(ScopedCounter),
50
+ scope.get(ScopedCounter),
51
+ scope.get(ScopedCounter),
52
+ )
53
+
54
+ unique_instances = set(results)
55
+ assert len(unique_instances) == 1, f"Expected 1 scoped instance within same scope, got {len(unique_instances)}"
56
+
57
+
58
+ async def test_scoped_concurrency_different_scopes() -> None:
59
+ container = create_async_container(services=[scoped_counter_factory])
60
+
61
+ async with container.enter_scope() as scope1:
62
+ async with container.enter_scope() as scope2:
63
+ res1, res2 = await asyncio.gather(scope1.get(ScopedCounter), scope2.get(ScopedCounter))
64
+
65
+ assert res1 is not res2, "Different scopes should produce different instances"
66
+
67
+
68
+ async def test_singleton_concurrency_cross_scope() -> None:
69
+ container = create_async_container(services=[counter_factory])
70
+
71
+ async with container.enter_scope() as scope1, container.enter_scope() as scope2:
72
+ # Concurrently request the SINGLETON from two different scopes
73
+ results = await asyncio.gather(
74
+ scope1.get(Counter),
75
+ scope1.get(Counter),
76
+ scope1.get(Counter),
77
+ scope2.get(Counter),
78
+ scope2.get(Counter),
79
+ scope2.get(Counter),
80
+ scope2.get(Counter),
81
+ container.get(Counter),
82
+ )
83
+
84
+ unique_instances = set(results)
85
+ assert len(unique_instances) == 1, f"Expected 1 singleton instance, got {len(unique_instances)}"
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import contextlib
2
4
  from typing import Iterator
3
5
 
@@ -76,10 +76,10 @@ def _get_startup_event(
76
76
  container: wireup.AsyncContainer,
77
77
  handlers: Optional[Iterable[Type[_WireupHandler]]],
78
78
  ) -> Callable[[web.Application], Awaitable[None]]:
79
- for handler_type in handlers or []:
80
- container._registry.extend(impls=[InjectableDeclaration(handler_type)])
81
- container._compiler.compile()
82
- container._scoped_compiler.compile()
79
+ if handlers:
80
+ for handler_type in handlers:
81
+ container._registry.extend(impls=[InjectableDeclaration(handler_type)])
82
+ container._recompile()
83
83
 
84
84
  async def _inner(app: web.Application) -> None:
85
85
  if handlers:
@@ -166,8 +166,7 @@ def _update_lifespan(
166
166
  if class_based_routes:
167
167
  for cbr in class_based_routes:
168
168
  container._registry.extend(impls=[InjectableDeclaration(cbr)])
169
- container._compiler.compile()
170
- container._scoped_compiler.compile()
169
+ container._recompile()
171
170
 
172
171
  for cbr in class_based_routes:
173
172
  await _instantiate_class_based_route(app, container, cbr)
@@ -93,7 +93,7 @@ def _create_container( # noqa: PLR0913
93
93
  singleton_compiler = FactoryCompiler(registry, is_scoped_container=False)
94
94
  scoped_compiler = FactoryCompiler(registry, is_scoped_container=True)
95
95
  singleton_compiler.compile()
96
- scoped_compiler.compile()
96
+ scoped_compiler.compile(copy_singletons_from=singleton_compiler)
97
97
 
98
98
  override_manager = OverrideManager(registry.is_type_with_qualifier_known, singleton_compiler, scoped_compiler)
99
99
  return klass(
@@ -17,6 +17,7 @@ from wireup.errors import (
17
17
  UnknownServiceRequestedError,
18
18
  WireupError,
19
19
  )
20
+ from wireup.ioc.container.lock_registry import LockRegistry
20
21
 
21
22
  if TYPE_CHECKING:
22
23
  from wireup.ioc.configuration import ConfigStore
@@ -40,6 +41,7 @@ class BaseContainer:
40
41
  "_factories",
41
42
  "_global_scope_exit_stack",
42
43
  "_global_scope_objects",
44
+ "_locks",
43
45
  "_override_mgr",
44
46
  "_registry",
45
47
  "_scoped_compiler",
@@ -65,6 +67,7 @@ class BaseContainer:
65
67
  self._compiler = factory_compiler
66
68
  self._scoped_compiler = scoped_compiler
67
69
  self._factories = self._compiler.factories
70
+ self._locks = LockRegistry({obj_id for obj_id, f in self._factories.items() if f.is_async})
68
71
 
69
72
  @property
70
73
  def params(self) -> ConfigStore:
@@ -111,3 +114,9 @@ class BaseContainer:
111
114
  return compiled_factory.factory(self) # type:ignore[no-any-return]
112
115
 
113
116
  raise UnknownServiceRequestedError(klass, qualifier)
117
+
118
+ def _recompile(self) -> None:
119
+ """Update internal container state after registry changes"""
120
+ self._compiler.compile()
121
+ self._scoped_compiler.compile(copy_singletons_from=self._compiler)
122
+ self._locks._async_hashes.update({obj_id for obj_id, f in self._factories.items() if f.is_async})
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import threading
5
+ from typing import Dict
6
+
7
+
8
+ class LockRegistry(Dict[int, "asyncio.Lock | threading.Lock"]):
9
+ __slots__ = ("_async_hashes", "_lock")
10
+
11
+ def __init__(self, async_hashes: set[int]) -> None:
12
+ super().__init__()
13
+ self._async_hashes = async_hashes
14
+ self._lock = threading.Lock()
15
+
16
+ def __missing__(self, key: int) -> asyncio.Lock | threading.Lock:
17
+ with self._lock:
18
+ if key not in self:
19
+ self[key] = asyncio.Lock() if key in self._async_hashes else threading.Lock()
20
+ return self[key]
@@ -0,0 +1,245 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import sys
5
+ import threading
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING, Any, Callable, Hashable
8
+
9
+ from wireup.codegen import Codegen
10
+ from wireup.errors import WireupError
11
+ from wireup.ioc.registry import GENERATOR_FACTORY_TYPES, ContainerRegistry, FactoryType
12
+ from wireup.ioc.types import ConfigInjectionRequest, TemplatedString
13
+ from wireup.util import format_name
14
+
15
+ if TYPE_CHECKING:
16
+ from wireup.ioc.container.base_container import BaseContainer
17
+ from wireup.ioc.registry import InjectableFactory
18
+
19
+
20
+ @dataclass(**({"slots": True} if sys.version_info >= (3, 10) else {}))
21
+ class CompiledFactory:
22
+ factory: Callable[[BaseContainer], Any]
23
+ is_async: bool
24
+
25
+
26
+ _CONTAINER_SCOPE_ERROR_MSG = (
27
+ "Scope mismatch: Cannot resolve {lifetime} injectable {fmt_klass} "
28
+ "from the root container. "
29
+ "Only Singleton injectables can be resolved without a scope. "
30
+ "To resolve {lifetime} injectables, you must create a scope.\n"
31
+ "See: https://maldoinc.github.io/wireup/latest/lifetimes_and_scopes/"
32
+ )
33
+
34
+ _WIREUP_GENERATED_FACTORY_NAME = "_wireup_factory"
35
+ _SENTINEL = object()
36
+
37
+
38
+ class FactoryCompiler:
39
+ def __init__(self, registry: ContainerRegistry, *, is_scoped_container: bool) -> None:
40
+ self._registry = registry
41
+ self._is_scoped_container = is_scoped_container
42
+ self.factories: dict[int, CompiledFactory] = {}
43
+
44
+ @classmethod
45
+ def get_object_id(cls, impl: type, qualifier: Hashable) -> int:
46
+ return hash(impl if qualifier is None else (impl, qualifier))
47
+
48
+ def compile(self, copy_singletons_from: FactoryCompiler | None = None) -> None:
49
+ for impl, qualifiers in self._registry.impls.items():
50
+ for qualifier in qualifiers:
51
+ obj_id = FactoryCompiler.get_object_id(impl, qualifier)
52
+
53
+ if obj_id in self.factories:
54
+ continue
55
+
56
+ if (
57
+ copy_singletons_from
58
+ and (compiled := copy_singletons_from.factories.get(obj_id))
59
+ and self._registry.lifetime[impl, qualifier] == "singleton"
60
+ ):
61
+ self.factories[obj_id] = compiled
62
+ continue
63
+
64
+ self.factories[obj_id] = self._compile_and_create_function(
65
+ self._registry.factories[impl, qualifier],
66
+ impl,
67
+ qualifier,
68
+ )
69
+
70
+ for interface, impls in self._registry.interfaces.items():
71
+ for qualifier, impl in impls.items():
72
+ obj_id = FactoryCompiler.get_object_id(interface, qualifier)
73
+
74
+ if obj_id in self.factories:
75
+ continue
76
+
77
+ if (
78
+ copy_singletons_from
79
+ and (compiled := copy_singletons_from.factories.get(obj_id))
80
+ and self._registry.lifetime[impl, qualifier] == "singleton"
81
+ ):
82
+ self.factories[obj_id] = compiled
83
+ continue
84
+
85
+ self.factories[obj_id] = self._compile_and_create_function(
86
+ self._registry.factories[impl, qualifier],
87
+ interface,
88
+ qualifier,
89
+ )
90
+
91
+ def _get_factory_code( # noqa: C901, PLR0915
92
+ self,
93
+ factory: InjectableFactory,
94
+ lifetime: str,
95
+ *,
96
+ is_interface: bool,
97
+ ) -> tuple[str, bool, bool]:
98
+ cg = Codegen()
99
+
100
+ maybe_async = "async " if factory.is_async else ""
101
+ cg += f"{maybe_async}def {_WIREUP_GENERATED_FACTORY_NAME}(container):"
102
+ cache_created_instance = lifetime != "transient"
103
+
104
+ def _generate_factory_body(cg: Codegen) -> None:
105
+ kwargs = ""
106
+ for name, dep in self._registry.dependencies[factory.factory].items():
107
+ if isinstance(dep.annotation, ConfigInjectionRequest):
108
+ param_value = (
109
+ str(dep.annotation.config_key)
110
+ if isinstance(dep.annotation.config_key, TemplatedString)
111
+ else f'"{dep.annotation.config_key}"'
112
+ )
113
+ cg += f"_obj_dep_{name} = parameters.get({param_value})"
114
+ else:
115
+ if self._registry.is_interface_known(dep.klass):
116
+ dep_class = self._registry.interface_resolve_impl(dep.klass, dep.qualifier_value)
117
+ else:
118
+ dep_class = dep.klass
119
+
120
+ maybe_await = "await " if self._registry.factories[dep_class, dep.qualifier_value].is_async else ""
121
+ dep_hash = FactoryCompiler.get_object_id(dep_class, dep.qualifier_value)
122
+ cg += f"_obj_dep_{name} = {maybe_await}factories[{dep_hash}].factory(container)"
123
+ kwargs += f"{name}=_obj_dep_{name}, "
124
+
125
+ maybe_await = "await " if factory.factory_type == FactoryType.COROUTINE_FN else ""
126
+
127
+ cg += f"instance = {maybe_await}ORIGINAL_FACTORY({kwargs.strip()})"
128
+
129
+ if factory.factory_type in GENERATOR_FACTORY_TYPES:
130
+ if lifetime == "singleton":
131
+ cg += "container._global_scope_exit_stack.append(instance)"
132
+ else:
133
+ cg += "container._current_scope_exit_stack.append(instance)"
134
+
135
+ if factory.factory_type == FactoryType.GENERATOR:
136
+ cg += "instance = next(instance)"
137
+ else:
138
+ cg += "instance = await instance.__anext__()"
139
+
140
+ if cache_created_instance:
141
+ cg += "storage[OBJ_ID] = instance"
142
+ if is_interface:
143
+ cg += "storage[ORIGINAL_OBJ_ID] = instance"
144
+
145
+ cg += "return instance"
146
+
147
+ with cg.indent():
148
+ if cache_created_instance:
149
+ if lifetime == "singleton":
150
+ cg += "storage = container._global_scope_objects"
151
+ else:
152
+ cg += "storage = container._current_scope_objects"
153
+
154
+ cg += "if (res := storage.get(OBJ_ID, _SENTINEL)) is not _SENTINEL:"
155
+ with cg.indent():
156
+ cg += "return res"
157
+
158
+ lock = "_singleton_lock" if lifetime == "singleton" else "container._locks[OBJ_HASH]"
159
+
160
+ if factory.is_async:
161
+ cg += f"async with {lock}:"
162
+ else:
163
+ cg += f"with {lock}:"
164
+
165
+ with cg.indent():
166
+ # Use Double-Checked locking for factories
167
+ # See: https://en.wikipedia.org/wiki/Double-checked_locking
168
+ cg += "if (res := storage.get(OBJ_ID, _SENTINEL)) is not _SENTINEL:"
169
+ with cg.indent():
170
+ cg += "return res"
171
+
172
+ _generate_factory_body(cg)
173
+ else:
174
+ _generate_factory_body(cg)
175
+
176
+ return cg.get_source(), factory.is_async, lifetime == "singleton"
177
+
178
+ def _compile_and_create_function(
179
+ self, factory: InjectableFactory, impl: type, qualifier: Hashable
180
+ ) -> CompiledFactory:
181
+ obj_id = impl, qualifier
182
+ resolved_obj_id = (
183
+ (self._registry.interface_resolve_impl(impl, qualifier), qualifier)
184
+ if self._registry.is_interface_known(impl)
185
+ else obj_id
186
+ )
187
+
188
+ is_interface = self._registry.is_interface_known(impl)
189
+
190
+ if is_interface:
191
+ lifetime = self._registry.lifetime[self._registry.interface_resolve_impl(impl, qualifier), qualifier]
192
+ else:
193
+ lifetime = self._registry.lifetime[impl, qualifier]
194
+
195
+ # Non-singleton types cannot be resolved from the root container. Return a factory that will simply error.
196
+ if lifetime != "singleton" and not self._is_scoped_container:
197
+ return CompiledFactory(
198
+ factory=FactoryCompiler._create_scope_mismatch_error_factory(impl, qualifier, lifetime),
199
+ is_async=False,
200
+ )
201
+
202
+ obj_hash = FactoryCompiler.get_object_id(impl, qualifier)
203
+ source, is_async, needs_global_lock = self._get_factory_code(
204
+ factory,
205
+ lifetime,
206
+ is_interface=is_interface,
207
+ )
208
+
209
+ try:
210
+ namespace: dict[str, Any] = {
211
+ "factories": self.factories,
212
+ "ORIGINAL_OBJ_ID": obj_id,
213
+ "OBJ_ID": resolved_obj_id,
214
+ "OBJ_HASH": obj_hash,
215
+ "ORIGINAL_FACTORY": self._registry.factories[resolved_obj_id].factory,
216
+ "TemplatedString": TemplatedString,
217
+ "WireupError": WireupError,
218
+ "_CONTAINER_SCOPE_ERROR_MSG": _CONTAINER_SCOPE_ERROR_MSG,
219
+ "_SENTINEL": _SENTINEL,
220
+ "parameters": self._registry.parameters,
221
+ }
222
+
223
+ if needs_global_lock:
224
+ namespace["_singleton_lock"] = asyncio.Lock() if is_async else threading.Lock()
225
+
226
+ compiled_code = compile(source, f"<{_WIREUP_GENERATED_FACTORY_NAME}_{obj_id}>", "exec")
227
+ exec(compiled_code, namespace) # noqa: S102
228
+
229
+ return CompiledFactory(factory=namespace[_WIREUP_GENERATED_FACTORY_NAME], is_async=is_async)
230
+
231
+ except Exception as e:
232
+ msg = f"Failed to compile generated factory {obj_id}: {e}"
233
+ raise WireupError(msg) from e
234
+
235
+ @staticmethod
236
+ def _create_scope_mismatch_error_factory(
237
+ impl: type,
238
+ qualifier: Hashable,
239
+ lifetime: str,
240
+ ) -> Callable[[BaseContainer], Any]:
241
+ def _factory(_: BaseContainer) -> Any:
242
+ msg = _CONTAINER_SCOPE_ERROR_MSG.format(fmt_klass=format_name(impl, qualifier), lifetime=lifetime)
243
+ raise WireupError(msg)
244
+
245
+ return _factory