wireup 2.0.1__tar.gz → 2.2.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 (37) hide show
  1. {wireup-2.0.1 → wireup-2.2.0}/PKG-INFO +32 -17
  2. {wireup-2.0.1 → wireup-2.2.0}/pyproject.toml +6 -1
  3. {wireup-2.0.1 → wireup-2.2.0}/readme.md +26 -14
  4. wireup-2.2.0/wireup/_decorators.py +156 -0
  5. {wireup-2.0.1 → wireup-2.2.0}/wireup/errors.py +17 -5
  6. {wireup-2.0.1 → wireup-2.2.0}/wireup/integration/aiohttp.py +8 -3
  7. wireup-2.2.0/wireup/integration/click.py +26 -0
  8. {wireup-2.0.1 → wireup-2.2.0}/wireup/integration/django/apps.py +4 -4
  9. {wireup-2.0.1 → wireup-2.2.0}/wireup/integration/fastapi.py +25 -70
  10. wireup-2.2.0/wireup/integration/starlette.py +115 -0
  11. wireup-2.2.0/wireup/ioc/_exit_stack.py +91 -0
  12. {wireup-2.0.1 → wireup-2.2.0}/wireup/ioc/container/__init__.py +32 -13
  13. wireup-2.2.0/wireup/ioc/container/async_container.py +84 -0
  14. wireup-2.2.0/wireup/ioc/container/base_container.py +93 -0
  15. {wireup-2.0.1 → wireup-2.2.0}/wireup/ioc/container/sync_container.py +19 -5
  16. wireup-2.2.0/wireup/ioc/factory_compiler.py +159 -0
  17. wireup-2.2.0/wireup/ioc/override_manager.py +139 -0
  18. {wireup-2.0.1 → wireup-2.2.0}/wireup/ioc/service_registry.py +151 -17
  19. {wireup-2.0.1 → wireup-2.2.0}/wireup/ioc/types.py +1 -6
  20. wireup-2.2.0/wireup/ioc/util.py +198 -0
  21. wireup-2.0.1/wireup/_async_to_sync.py +0 -34
  22. wireup-2.0.1/wireup/_decorators.py +0 -110
  23. wireup-2.0.1/wireup/ioc/_exit_stack.py +0 -54
  24. wireup-2.0.1/wireup/ioc/container/async_container.py +0 -49
  25. wireup-2.0.1/wireup/ioc/container/base_container.py +0 -191
  26. wireup-2.0.1/wireup/ioc/override_manager.py +0 -75
  27. wireup-2.0.1/wireup/ioc/util.py +0 -81
  28. wireup-2.0.1/wireup/ioc/validation.py +0 -131
  29. {wireup-2.0.1 → wireup-2.2.0}/wireup/__init__.py +0 -0
  30. {wireup-2.0.1 → wireup-2.2.0}/wireup/_annotations.py +0 -0
  31. {wireup-2.0.1 → wireup-2.2.0}/wireup/_discovery.py +0 -0
  32. {wireup-2.0.1 → wireup-2.2.0}/wireup/integration/__init__.py +0 -0
  33. {wireup-2.0.1 → wireup-2.2.0}/wireup/integration/django/__init__.py +0 -0
  34. {wireup-2.0.1 → wireup-2.2.0}/wireup/integration/flask.py +0 -0
  35. {wireup-2.0.1 → wireup-2.2.0}/wireup/ioc/__init__.py +0 -0
  36. {wireup-2.0.1 → wireup-2.2.0}/wireup/ioc/parameter.py +0 -0
  37. {wireup-2.0.1 → wireup-2.2.0}/wireup/py.typed +0 -0
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: wireup
3
- Version: 2.0.1
3
+ Version: 2.2.0
4
4
  Summary: Python Dependency Injection Library
5
- Home-page: https://github.com/maldoinc/wireup
6
5
  License: MIT
7
6
  Keywords: flask,django,injector,dependency injection,dependency injection container,dependency injector
8
7
  Author: Aldo Mateli
@@ -26,12 +25,16 @@ Classifier: Programming Language :: Python :: 3.10
26
25
  Classifier: Programming Language :: Python :: 3.11
27
26
  Classifier: Programming Language :: Python :: 3.12
28
27
  Classifier: Programming Language :: Python :: 3.13
28
+ Classifier: Programming Language :: Python :: 3.14
29
29
  Classifier: Programming Language :: Python :: 3 :: Only
30
30
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
31
31
  Classifier: Typing :: Typed
32
+ Provides-Extra: eval-type
33
+ Requires-Dist: eval-type-backport (>=0.2.0,<0.3.0) ; extra == "eval-type"
32
34
  Requires-Dist: typing_extensions (>=4.7,<5.0)
33
35
  Project-URL: Changelog, https://github.com/maldoinc/wireup/releases
34
36
  Project-URL: Documentation, https://maldoinc.github.io/wireup/
37
+ Project-URL: Homepage, https://github.com/maldoinc/wireup
35
38
  Project-URL: Repository, https://github.com/maldoinc/wireup
36
39
  Description-Content-Type: text/markdown
37
40
 
@@ -43,8 +46,7 @@ Description-Content-Type: text/markdown
43
46
  [![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/maldoinc/wireup/run_all.yml)](https://github.com/maldoinc/wireup)
44
47
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/wireup)](https://pypi.org/project/wireup/)
45
48
  [![PyPI - Version](https://img.shields.io/pypi/v/wireup)](https://pypi.org/project/wireup/)
46
-
47
- <p><a target="_blank" href="https://maldoinc.github.io/wireup">📚 Documentation</a> | <a target="_blank" href="https://github.com/maldoinc/wireup-demo">🎮 Demo Application</a></p>
49
+ [![Documentation](https://img.shields.io/badge/%F0%9F%93%9A%20Documentation-3D9970)](https://maldoinc.github.io/wireup)
48
50
  </div>
49
51
 
50
52
  Dependency Injection (DI) is a design pattern where dependencies are provided externally rather than created within objects. Wireup automates dependency management using Python's type system, with support for async, generators, modern Python features and integrations for FastAPI, Django, Flask and AIOHTTP out of the box.
@@ -74,19 +76,32 @@ user_service = container.get(UserService) # ✅ Dependencies resolved.
74
76
  ```
75
77
 
76
78
  <details>
77
- <summary>Example With Configuration</summary>
79
+ <summary>No annotations</summary>
80
+
81
+ Keep domain objects clean of framework annotations by using factories.
78
82
 
79
83
  ```python
80
- @service
84
+ # Clean domain objects: No annotations
81
85
  class Database:
82
- def __init__(self, db_url: Annotated[str, Inject(param="db_url")]) -> None:
83
- self.db_url = db_url
86
+ pass
87
+
88
+ class UserService:
89
+ def __init__(self, db: Database) -> None:
90
+ self.db = db
91
+
92
+ # Register services via factories
93
+ @service
94
+ def database_factory() -> Database:
95
+ return Database()
96
+
97
+ @service
98
+ def user_service_factory(db: Database) -> UserService:
99
+ return UserService(db)
84
100
 
85
101
  container = wireup.create_sync_container(
86
- services=[Database],
87
- parameters={"db_url": os.environ["APP_DB_URL"]}
102
+ services=[database_factory, user_service_factory]
88
103
  )
89
- database = container.get(Database) # ✅ Dependencies resolved.
104
+ user_service = container.get(UserService) # ✅ Dependencies resolved.
90
105
  ```
91
106
 
92
107
  </details>
@@ -228,7 +243,7 @@ Wireup provides its own Dependency Injection mechanism and is not tied to specif
228
243
 
229
244
  Share the service layer between your web application and its accompanying CLI using Wireup.
230
245
 
231
- ### 🔌 Native Integration with Django, FastAPI, Flask and AIOHTTP
246
+ ### 🔌 Native Integration with Django, FastAPI, Flask, AIOHTTP, Click and Starlette
232
247
 
233
248
  Integrate with popular frameworks for a smoother developer experience.
234
249
  Integrations manage request scopes, injection in endpoints, and lifecycle of services.
@@ -244,6 +259,10 @@ def users_list(user_service: Injected[UserService]):
244
259
  wireup.integration.fastapi.setup(container, app)
245
260
  ```
246
261
 
262
+ **Supported Frameworks:** FastAPI (with zero-overhead class-based handlers), Django, Flask, AIOHTTP, Starlette, and Click.
263
+
264
+ [View all integrations →](https://maldoinc.github.io/wireup/latest/integrations/)
265
+
247
266
  ### 🧪 Simplified Testing
248
267
 
249
268
  Wireup does not patch your services and lets you test them in isolation.
@@ -263,7 +282,3 @@ with container.override.service(target=Database, new=in_memory_database):
263
282
 
264
283
  For more information [check out the documentation](https://maldoinc.github.io/wireup)
265
284
 
266
- ## 🎮 Demo application
267
-
268
- A demo flask application is available at [maldoinc/wireup-demo](https://github.com/maldoinc/wireup-demo)
269
-
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "wireup"
3
- version = "2.0.1"
3
+ version = "2.2.0"
4
4
  description = "Python Dependency Injection Library"
5
5
  authors = ["Aldo Mateli <aldo.mateli@gmail.com>"]
6
6
  license = "MIT"
@@ -46,6 +46,10 @@ Changelog = "https://github.com/maldoinc/wireup/releases"
46
46
  [tool.poetry.dependencies]
47
47
  python = "^3.8"
48
48
  typing_extensions = "^4.7"
49
+ eval-type-backport = { version = "^0.2.0", optional = true }
50
+
51
+ [tool.poetry.extras]
52
+ eval-type = ["eval-type-backport"]
49
53
 
50
54
  [tool.poetry.group.dev.dependencies]
51
55
  ruff = "0.11.0"
@@ -131,3 +135,4 @@ exclude = "wireup.integration"
131
135
 
132
136
  [tool.pytest.ini_options]
133
137
  asyncio_mode = "auto"
138
+ asyncio_default_fixture_loop_scope = "function"
@@ -6,8 +6,7 @@
6
6
  [![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/maldoinc/wireup/run_all.yml)](https://github.com/maldoinc/wireup)
7
7
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/wireup)](https://pypi.org/project/wireup/)
8
8
  [![PyPI - Version](https://img.shields.io/pypi/v/wireup)](https://pypi.org/project/wireup/)
9
-
10
- <p><a target="_blank" href="https://maldoinc.github.io/wireup">📚 Documentation</a> | <a target="_blank" href="https://github.com/maldoinc/wireup-demo">🎮 Demo Application</a></p>
9
+ [![Documentation](https://img.shields.io/badge/%F0%9F%93%9A%20Documentation-3D9970)](https://maldoinc.github.io/wireup)
11
10
  </div>
12
11
 
13
12
  Dependency Injection (DI) is a design pattern where dependencies are provided externally rather than created within objects. Wireup automates dependency management using Python's type system, with support for async, generators, modern Python features and integrations for FastAPI, Django, Flask and AIOHTTP out of the box.
@@ -37,19 +36,32 @@ user_service = container.get(UserService) # ✅ Dependencies resolved.
37
36
  ```
38
37
 
39
38
  <details>
40
- <summary>Example With Configuration</summary>
39
+ <summary>No annotations</summary>
40
+
41
+ Keep domain objects clean of framework annotations by using factories.
41
42
 
42
43
  ```python
43
- @service
44
+ # Clean domain objects: No annotations
44
45
  class Database:
45
- def __init__(self, db_url: Annotated[str, Inject(param="db_url")]) -> None:
46
- self.db_url = db_url
46
+ pass
47
+
48
+ class UserService:
49
+ def __init__(self, db: Database) -> None:
50
+ self.db = db
51
+
52
+ # Register services via factories
53
+ @service
54
+ def database_factory() -> Database:
55
+ return Database()
56
+
57
+ @service
58
+ def user_service_factory(db: Database) -> UserService:
59
+ return UserService(db)
47
60
 
48
61
  container = wireup.create_sync_container(
49
- services=[Database],
50
- parameters={"db_url": os.environ["APP_DB_URL"]}
62
+ services=[database_factory, user_service_factory]
51
63
  )
52
- database = container.get(Database) # ✅ Dependencies resolved.
64
+ user_service = container.get(UserService) # ✅ Dependencies resolved.
53
65
  ```
54
66
 
55
67
  </details>
@@ -191,7 +203,7 @@ Wireup provides its own Dependency Injection mechanism and is not tied to specif
191
203
 
192
204
  Share the service layer between your web application and its accompanying CLI using Wireup.
193
205
 
194
- ### 🔌 Native Integration with Django, FastAPI, Flask and AIOHTTP
206
+ ### 🔌 Native Integration with Django, FastAPI, Flask, AIOHTTP, Click and Starlette
195
207
 
196
208
  Integrate with popular frameworks for a smoother developer experience.
197
209
  Integrations manage request scopes, injection in endpoints, and lifecycle of services.
@@ -207,6 +219,10 @@ def users_list(user_service: Injected[UserService]):
207
219
  wireup.integration.fastapi.setup(container, app)
208
220
  ```
209
221
 
222
+ **Supported Frameworks:** FastAPI (with zero-overhead class-based handlers), Django, Flask, AIOHTTP, Starlette, and Click.
223
+
224
+ [View all integrations →](https://maldoinc.github.io/wireup/latest/integrations/)
225
+
210
226
  ### 🧪 Simplified Testing
211
227
 
212
228
  Wireup does not patch your services and lets you test them in isolation.
@@ -225,7 +241,3 @@ with container.override.service(target=Database, new=in_memory_database):
225
241
  ## 📚 Documentation
226
242
 
227
243
  For more information [check out the documentation](https://maldoinc.github.io/wireup)
228
-
229
- ## 🎮 Demo application
230
-
231
- A demo flask application is available at [maldoinc/wireup-demo](https://github.com/maldoinc/wireup-demo)
@@ -0,0 +1,156 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import functools
5
+ import inspect
6
+ from contextlib import AsyncExitStack, ExitStack
7
+ from typing import TYPE_CHECKING, Any, Callable
8
+
9
+ from wireup.errors import WireupError
10
+ from wireup.ioc.container.async_container import AsyncContainer, ScopedAsyncContainer, async_container_force_sync_scope
11
+ from wireup.ioc.container.sync_container import SyncContainer
12
+ from wireup.ioc.types import AnnotatedParameter, ParameterWrapper
13
+ from wireup.ioc.util import (
14
+ get_inject_annotated_parameters,
15
+ get_valid_injection_annotated_parameters,
16
+ )
17
+
18
+ if TYPE_CHECKING:
19
+ from wireup.ioc.container.sync_container import ScopedSyncContainer
20
+
21
+
22
+ def inject_from_container_unchecked(
23
+ scoped_container_supplier: Callable[[], ScopedSyncContainer | ScopedAsyncContainer],
24
+ ) -> Callable[..., Any]:
25
+ """Inject dependencies into the decorated function. The "unchecked" part of the name refers to the fact that
26
+ this cannot perform validation on the parameters to inject on module import time due to the absence of a container
27
+ instance."""
28
+
29
+ def _decorator(target: Callable[..., Any]) -> Callable[..., Any]:
30
+ return inject_from_container_util(
31
+ target=target,
32
+ names_to_inject=get_inject_annotated_parameters(target),
33
+ container=None,
34
+ scoped_container_supplier=scoped_container_supplier,
35
+ middleware=None,
36
+ )
37
+
38
+ return _decorator
39
+
40
+
41
+ def inject_from_container(
42
+ container: SyncContainer | AsyncContainer,
43
+ scoped_container_supplier: Callable[[], ScopedSyncContainer | ScopedAsyncContainer] | None = None,
44
+ middleware: Callable[
45
+ [ScopedSyncContainer | ScopedAsyncContainer, tuple[Any, ...], dict[str, Any]],
46
+ contextlib.AbstractContextManager[None],
47
+ ]
48
+ | None = None,
49
+ ) -> Callable[..., Any]:
50
+ """Inject dependencies into the decorated function based on annotations.
51
+
52
+ :param container: The main container instance created via `wireup.create_sync_container` or
53
+ `wireup.create_async_container`.
54
+ :param scoped_container_supplier: An optional callable that returns the current scoped container instance.
55
+ If provided, it will be used to create scoped dependencies. If not provided, the container will automatically
56
+ enter a scope. Provide a scoped_container_supplier if you need to manage the container's scope manually. For
57
+ example, in web frameworks, you might enter the scope at the start of a request in middleware so that other
58
+ middlewares can access the scoped container if needed.
59
+ :param middleware: A context manager that wraps the execution of the target function.
60
+ """
61
+
62
+ def _decorator(target: Callable[..., Any]) -> Callable[..., Any]:
63
+ if inspect.iscoroutinefunction(target) and isinstance(container, SyncContainer):
64
+ msg = (
65
+ "Sync container cannot perform injection on async targets. "
66
+ "Create an async container via wireup.create_async_container."
67
+ )
68
+ raise WireupError(msg)
69
+
70
+ return inject_from_container_util(
71
+ target=target,
72
+ names_to_inject=get_valid_injection_annotated_parameters(container, target),
73
+ container=container,
74
+ scoped_container_supplier=scoped_container_supplier,
75
+ middleware=middleware,
76
+ )
77
+
78
+ return _decorator
79
+
80
+
81
+ def inject_from_container_util( # noqa: C901
82
+ target: Callable[..., Any],
83
+ names_to_inject: dict[str, AnnotatedParameter],
84
+ container: SyncContainer | AsyncContainer | None,
85
+ scoped_container_supplier: Callable[[], ScopedSyncContainer | ScopedAsyncContainer] | None = None,
86
+ middleware: Callable[
87
+ [ScopedSyncContainer | ScopedAsyncContainer, tuple[Any, ...], dict[str, Any]],
88
+ contextlib.AbstractContextManager[None],
89
+ ]
90
+ | None = None,
91
+ ) -> Callable[..., Any]:
92
+ if not (container or scoped_container_supplier):
93
+ msg = "Container or scoped_container_supplier must be provided for injection."
94
+ raise WireupError(msg)
95
+
96
+ if not names_to_inject:
97
+ return target
98
+
99
+ if inspect.iscoroutinefunction(target):
100
+
101
+ @functools.wraps(target)
102
+ async def _inject_async_target(*args: Any, **kwargs: Any) -> Any:
103
+ async with AsyncExitStack() as cm:
104
+ if scoped_container_supplier:
105
+ scoped_container = scoped_container_supplier()
106
+ elif container:
107
+ scoped_container = await cm.enter_async_context(container.enter_scope()) # type: ignore[reportArgumentType, arg-type, unused-ignore]
108
+ else:
109
+ msg = "scoped_container_supplier or container must be provided for injection."
110
+ raise ValueError(msg)
111
+
112
+ if middleware:
113
+ cm.enter_context(middleware(scoped_container, args, kwargs))
114
+
115
+ injected_names = {
116
+ name: scoped_container.params.get(param.annotation.param)
117
+ if isinstance(param.annotation, ParameterWrapper)
118
+ else await scoped_container.get(param.klass, qualifier=param.qualifier_value)
119
+ for name, param in names_to_inject.items()
120
+ if param.annotation
121
+ }
122
+
123
+ return await target(*args, **{**kwargs, **injected_names})
124
+
125
+ return _inject_async_target
126
+
127
+ @functools.wraps(target)
128
+ def _inject_target(*args: Any, **kwargs: Any) -> Any:
129
+ with ExitStack() as cm:
130
+ if scoped_container_supplier:
131
+ scoped_container = scoped_container_supplier()
132
+ elif container:
133
+ scoped_container = cm.enter_context(
134
+ container.enter_scope()
135
+ if isinstance(container, SyncContainer)
136
+ else async_container_force_sync_scope(container)
137
+ )
138
+ else:
139
+ msg = "scoped_container_supplier or container must be provided for injection."
140
+ raise ValueError(msg)
141
+
142
+ if middleware:
143
+ cm.enter_context(middleware(scoped_container, args, kwargs))
144
+
145
+ get = scoped_container._synchronous_get
146
+
147
+ injected_names = {
148
+ name: scoped_container.params.get(param.annotation.param)
149
+ if isinstance(param.annotation, ParameterWrapper)
150
+ else get(param.klass, qualifier=param.qualifier_value)
151
+ for name, param in names_to_inject.items()
152
+ if param.annotation
153
+ }
154
+ return target(*args, **{**kwargs, **injected_names})
155
+
156
+ return _inject_target
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import sys
3
4
  from typing import TYPE_CHECKING, Any
4
5
 
5
6
  if TYPE_CHECKING:
@@ -90,9 +91,20 @@ class UnknownOverrideRequestedError(WireupError):
90
91
  super().__init__(f"Cannot override unknown {klass} with qualifier '{qualifier}'.")
91
92
 
92
93
 
93
- class ContainerCloseError(WireupError):
94
- """Contains a list of exceptions raised while closing the container."""
94
+ if sys.version_info >= (3, 11):
95
95
 
96
- def __init__(self, errors: list[Exception]) -> None:
97
- self.errors = errors
98
- super().__init__(f"The following exceptions were raised while closing the container: {errors}")
96
+ class ContainerCloseError(ExceptionGroup, WireupError): # noqa: F821
97
+ """Contains a list of exceptions raised while closing the container."""
98
+
99
+ def __init__(self, message: str, errors: list[Exception]) -> None:
100
+ self.errors = errors
101
+ super().__init__(message, errors)
102
+
103
+ else:
104
+
105
+ class ContainerCloseError(WireupError):
106
+ """Contains a list of exceptions raised while closing the container."""
107
+
108
+ def __init__(self, message: str, errors: list[Exception]) -> None:
109
+ self.errors = errors
110
+ super().__init__(f"{message}: {errors}")
@@ -73,10 +73,13 @@ async def _instantiate_class_based_handlers(
73
73
 
74
74
 
75
75
  def _get_startup_event(
76
- container: wireup.AsyncContainer, handlers: Optional[Iterable[Type[_WireupHandler]]]
76
+ container: wireup.AsyncContainer,
77
+ handlers: Optional[Iterable[Type[_WireupHandler]]],
77
78
  ) -> Callable[[web.Application], Awaitable[None]]:
78
79
  for handler_type in handlers or []:
79
- container._registry._extend_with_services(abstracts=[], impls=[ServiceDeclaration(handler_type)])
80
+ container._registry.extend(impls=[ServiceDeclaration(handler_type)])
81
+ container._compiler.compile()
82
+ container._scoped_compiler.compile()
80
83
 
81
84
  async def _inner(app: web.Application) -> None:
82
85
  if handlers:
@@ -88,7 +91,9 @@ def _get_startup_event(
88
91
 
89
92
 
90
93
  def setup(
91
- container: wireup.AsyncContainer, app: web.Application, handlers: Optional[Iterable[Type[_WireupHandler]]] = None
94
+ container: wireup.AsyncContainer,
95
+ app: web.Application,
96
+ handlers: Optional[Iterable[Type[_WireupHandler]]] = None,
92
97
  ) -> None:
93
98
  """Integrate Wireup with AIOHTTP.
94
99
 
@@ -0,0 +1,26 @@
1
+ from click import Group
2
+
3
+ from wireup import SyncContainer, inject_from_container
4
+
5
+
6
+ def _inject_commands(container: SyncContainer, group: Group) -> None:
7
+ for command in group.commands.values():
8
+ if fn := command.callback:
9
+ command.callback = inject_from_container(container)(fn)
10
+
11
+ if isinstance(command, Group):
12
+ _inject_commands(container, command)
13
+
14
+
15
+ def setup(container: SyncContainer, command: Group) -> None:
16
+ """Integrate Wireup with Click by injecting dependencies into Click commands.
17
+
18
+ :command: The Click command group to inject dependencies into
19
+ """
20
+ _inject_commands(container, command)
21
+ command.wireup_container = container # type: ignore[reportAttributeAccessIssue]
22
+
23
+
24
+ def get_app_container(app: Group) -> SyncContainer:
25
+ """Retrieve the Wireup container associated with a Click command group."""
26
+ return app.wireup_container # type: ignore[reportAttributeAccessIssue]
@@ -1,6 +1,6 @@
1
- import asyncio
2
1
  import functools
3
2
  import importlib
3
+ import inspect
4
4
  from contextvars import ContextVar
5
5
  from dataclasses import dataclass
6
6
  from types import ModuleType
@@ -21,7 +21,7 @@ from wireup.errors import WireupError
21
21
  from wireup.ioc.container.async_container import AsyncContainer, ScopedAsyncContainer, async_container_force_sync_scope
22
22
  from wireup.ioc.container.sync_container import ScopedSyncContainer
23
23
  from wireup.ioc.types import ParameterWrapper
24
- from wireup.ioc.validation import get_valid_injection_annotated_parameters
24
+ from wireup.ioc.util import get_valid_injection_annotated_parameters
25
25
 
26
26
  if TYPE_CHECKING:
27
27
  from wireup.integration.django import WireupSettings
@@ -38,7 +38,7 @@ def wireup_middleware(
38
38
  ) -> Callable[[HttpRequest], Union[HttpResponse, Awaitable[HttpResponse]]]:
39
39
  container = get_app_container()
40
40
 
41
- if asyncio.iscoroutinefunction(get_response):
41
+ if inspect.iscoroutinefunction(get_response):
42
42
 
43
43
  async def async_inner(request: HttpRequest) -> HttpResponse:
44
44
  async with container.enter_scope() as scoped:
@@ -65,7 +65,7 @@ def wireup_middleware(
65
65
  return sync_inner
66
66
 
67
67
 
68
- @service
68
+ @service(lifetime="scoped")
69
69
  def _django_request_factory() -> HttpRequest:
70
70
  try:
71
71
  return current_request.get()
@@ -1,9 +1,7 @@
1
1
  import contextlib
2
- from contextvars import ContextVar
3
2
  from typing import (
4
3
  Any,
5
4
  AsyncIterator,
6
- Awaitable,
7
5
  Callable,
8
6
  Dict,
9
7
  Iterable,
@@ -16,26 +14,38 @@ from typing import (
16
14
  )
17
15
 
18
16
  import fastapi
19
- from fastapi import FastAPI, Request, Response, WebSocket
20
- from fastapi.requests import HTTPConnection
17
+ from fastapi import FastAPI
21
18
  from fastapi.routing import APIRoute, APIWebSocketRoute
22
- from starlette.middleware.base import BaseHTTPMiddleware
23
19
  from starlette.routing import BaseRoute
24
20
  from typing_extensions import Protocol
25
21
 
26
- from wireup import inject_from_container, service
22
+ from wireup import inject_from_container
27
23
  from wireup._annotations import ServiceDeclaration
28
24
  from wireup.errors import WireupError
25
+ from wireup.integration.starlette import (
26
+ WireupAsgiMiddleware,
27
+ current_request,
28
+ get_app_container,
29
+ get_request_container,
30
+ request_factory,
31
+ websocket_factory,
32
+ )
29
33
  from wireup.ioc.container.async_container import AsyncContainer, ScopedAsyncContainer
30
34
  from wireup.ioc.container.sync_container import ScopedSyncContainer
31
35
  from wireup.ioc.types import AnyCallable
32
- from wireup.ioc.validation import (
33
- assert_dependencies_valid,
36
+ from wireup.ioc.util import (
34
37
  get_inject_annotated_parameters,
35
38
  hide_annotated_names,
36
39
  )
37
40
 
38
- current_request: ContextVar[HTTPConnection] = ContextVar("wireup_fastapi_request")
41
+ __all__ = [
42
+ "WireupRoute",
43
+ "get_app_container",
44
+ "get_request_container",
45
+ "request_factory",
46
+ "setup",
47
+ "websocket_factory",
48
+ ]
39
49
 
40
50
 
41
51
  class _ClassBasedHandlersProtocol(Protocol):
@@ -48,50 +58,6 @@ class WireupRoute(APIRoute):
48
58
  super().__init__(path=path, endpoint=endpoint, **kwargs)
49
59
 
50
60
 
51
- @service(lifetime="scoped")
52
- def fastapi_request_factory() -> Request:
53
- """Provide the current FastAPI request as a dependency.
54
-
55
- Note that this requires the Wireup-FastAPI integration to be set up.
56
- """
57
- msg = "fastapi.Request in Wireup is only available during a request."
58
- try:
59
- res = current_request.get()
60
- if not isinstance(res, Request):
61
- raise WireupError(msg)
62
-
63
- return res
64
- except LookupError as e:
65
- raise WireupError(msg) from e
66
-
67
-
68
- @service(lifetime="scoped")
69
- def fastapi_websocket_factory() -> WebSocket:
70
- """Provide the current FastAPI WebSocket as a dependency.
71
-
72
- Note that this requires the Wireup-FastAPI integration to be set up.
73
- """
74
- msg = "fastapi.WebSocket in Wireup is only available in a websocket connection."
75
- try:
76
- res = current_request.get()
77
- if not isinstance(res, WebSocket):
78
- raise WireupError(msg)
79
-
80
- return res
81
- except LookupError as e:
82
- raise WireupError(msg) from e
83
-
84
-
85
- async def _wireup_request_middleware(request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
86
- token = current_request.set(request)
87
- try:
88
- async with request.app.state.wireup_container.enter_scope() as scoped_container:
89
- request.state.wireup_container = scoped_container
90
- return await call_next(request)
91
- finally:
92
- current_request.reset(token)
93
-
94
-
95
61
  def _inject_fastapi_route(
96
62
  *,
97
63
  container: AsyncContainer,
@@ -100,14 +66,13 @@ def _inject_fastapi_route(
100
66
  remove_http_connection_from_arguments: bool,
101
67
  add_custom_middleware: bool,
102
68
  ) -> AnyCallable:
103
- # Warn: Make sure the logic evolves with the _wireup_request_middleware function.
104
69
  @contextlib.contextmanager
105
70
  def _request_middleware(
106
71
  scoped_container: Union[ScopedAsyncContainer, ScopedSyncContainer],
107
72
  _args: Tuple[Any, ...],
108
73
  kwargs: Dict[str, Any],
109
74
  ) -> Iterator[None]:
110
- request: HTTPConnection = kwargs[http_connection_param_name]
75
+ request = kwargs[http_connection_param_name]
111
76
  request.state.wireup_container = scoped_container
112
77
  token = current_request.set(request)
113
78
  try:
@@ -135,8 +100,7 @@ def _inject_routes(container: AsyncContainer, routes: List[BaseRoute], *, is_usi
135
100
  route.dependant.call = inject_from_container(container, get_request_container)(route.dependant.call)
136
101
  continue
137
102
 
138
- # This is now either a websocket route
139
- # or an APIRoute but the asgi middleware is not used.
103
+ # This is now either a websocket route or an APIRoute but the asgi middleware is not used.
140
104
  # In this case we need to use the custom route middleware to extract the current request/websocket.
141
105
  add_custom_middleware = isinstance(route, APIWebSocketRoute) or not is_using_asgi_middleware
142
106
  is_http_connection_in_signature = route.dependant.http_connection_param_name is not None
@@ -201,8 +165,9 @@ def _update_lifespan(
201
165
  async def lifespan(app: FastAPI) -> AsyncIterator[Any]:
202
166
  if class_based_routes:
203
167
  for cbr in class_based_routes:
204
- container._registry._extend_with_services(abstracts=[], impls=[ServiceDeclaration(cbr)])
205
- assert_dependencies_valid(container)
168
+ container._registry.extend(impls=[ServiceDeclaration(cbr)])
169
+ container._compiler.compile()
170
+ container._scoped_compiler.compile()
206
171
 
207
172
  for cbr in class_based_routes:
208
173
  await _instantiate_class_based_route(app, container, cbr)
@@ -242,7 +207,7 @@ def setup(
242
207
  """
243
208
  app.state.wireup_container = container
244
209
  if middleware_mode:
245
- app.add_middleware(BaseHTTPMiddleware, dispatch=_wireup_request_middleware)
210
+ app.add_middleware(WireupAsgiMiddleware)
246
211
  _update_lifespan(
247
212
  app,
248
213
  class_based_routes=class_based_handlers,
@@ -253,13 +218,3 @@ def setup(
253
218
  # If no class-based handlers are used, we inject them immediately.
254
219
  if not class_based_handlers:
255
220
  _inject_routes(container, app.routes, is_using_asgi_middleware=middleware_mode)
256
-
257
-
258
- def get_app_container(app: FastAPI) -> AsyncContainer:
259
- """Return the container associated with the given FastAPI application."""
260
- return app.state.wireup_container
261
-
262
-
263
- def get_request_container() -> ScopedAsyncContainer:
264
- """When inside a request, returns the scoped container instance handling the current request."""
265
- return current_request.get().state.wireup_container