From ae301913a55b26d9c1ea386709916b5b467a2f9f Mon Sep 17 00:00:00 2001 From: Aldo Mateli Date: Fri, 22 Dec 2023 21:03:54 +0000 Subject: [PATCH] Add fastapi integration --- test/integration/test_fastapi_integration.py | 6 ++- wireup/integration/fastapi_integration.py | 42 ++++++++++++++++++++ wireup/integration/flask_integration.py | 17 ++------ wireup/integration/util.py | 19 +++++++++ 4 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 wireup/integration/fastapi_integration.py create mode 100644 wireup/integration/util.py diff --git a/test/integration/test_fastapi_integration.py b/test/integration/test_fastapi_integration.py index 78ae354..ee9de77 100644 --- a/test/integration/test_fastapi_integration.py +++ b/test/integration/test_fastapi_integration.py @@ -7,6 +7,7 @@ from test.services.no_annotations.random.random_service import RandomService from wireup import Wire, ParameterBag, DependencyContainer from wireup.errors import UnknownServiceRequestedError +from wireup.integration.fastapi_integration import wireup_init_fastapi_integration class TestFastAPI(unittest.TestCase): @@ -19,20 +20,21 @@ def test_injects_service(self): self.container.register(RandomService) @self.app.get("/") - @self.container.autowire async def target(random_service: Annotated[RandomService, Wire()]): return {"number": random_service.get_random()} + wireup_init_fastapi_integration(self.app, dependency_container=self.container, service_modules=[]) response = self.client.get("/") self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"number": 4}) def test_raises_on_unknown_service(self): @self.app.get("/") - @self.container.autowire async def target(_unknown_service: Annotated[unittest.TestCase, Wire()]): return {"msg": "Hello World"} + wireup_init_fastapi_integration(self.app, dependency_container=self.container, service_modules=[]) + with self.assertRaises(UnknownServiceRequestedError) as e: self.client.get("/") diff --git a/wireup/integration/fastapi_integration.py b/wireup/integration/fastapi_integration.py new file mode 100644 index 0000000..2582c90 --- /dev/null +++ b/wireup/integration/fastapi_integration.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from fastapi.routing import APIRoute + +from wireup import DependencyContainer, container, warmup_container +from wireup.integration.util import is_view_using_container + +if TYPE_CHECKING: + from types import ModuleType + + from fastapi import FastAPI + + +def wireup_init_fastapi_integration( + app: FastAPI, + service_modules: list[ModuleType], + dependency_container: DependencyContainer = container, +) -> None: + """Integrate wireup with a fastapi application. + + This must be called once all views have been registered. + Decorates all views where container objects are being used making + the `@container.autowire` decorator no longer needed. + + :param app: The application instance + :param service_modules: A list of python modules where application services reside. These will be loaded to trigger + container registrations. + :param dependency_container: The instance of the dependency container. + The default wireup singleton will be used when this is unset. + This will be a noop and have no performance penalty for views which do not use the container. + """ + warmup_container(dependency_container, service_modules or []) + + for route in app.routes: + if ( + isinstance(route, APIRoute) + and route.dependant.call + and is_view_using_container(dependency_container, route.dependant.call) + ): + route.dependant.call = dependency_container.autowire(route.endpoint) diff --git a/wireup/integration/flask_integration.py b/wireup/integration/flask_integration.py index 07b0973..d31b6e6 100644 --- a/wireup/integration/flask_integration.py +++ b/wireup/integration/flask_integration.py @@ -1,9 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any from wireup import DependencyContainer, container, warmup_container -from wireup.ioc.types import InjectableType +from wireup.integration.util import is_view_using_container if TYPE_CHECKING: from types import ModuleType @@ -11,17 +11,6 @@ from flask import Flask -def _is_view_using_container(dependency_container: DependencyContainer, view: Callable[..., Any]) -> bool: - if hasattr(view, "__annotations__"): - for dep in set(view.__annotations__.values()): - is_requesting_injection = hasattr(dep, "__metadata__") and isinstance(dep.__metadata__[0], InjectableType) - - if is_requesting_injection or dependency_container.is_type_known(dep): - return True - - return False - - def wireup_init_flask_integration( flask_app: Flask, service_modules: list[ModuleType], @@ -51,6 +40,6 @@ def wireup_init_flask_integration( warmup_container(dependency_container, service_modules or []) flask_app.view_functions = { - name: dependency_container.autowire(view) if _is_view_using_container(dependency_container, view) else view + name: dependency_container.autowire(view) if is_view_using_container(dependency_container, view) else view for name, view in flask_app.view_functions.items() } diff --git a/wireup/integration/util.py b/wireup/integration/util.py new file mode 100644 index 0000000..def6d26 --- /dev/null +++ b/wireup/integration/util.py @@ -0,0 +1,19 @@ +import inspect +from typing import Any, Callable + +from wireup import DependencyContainer +from wireup.ioc.types import InjectableType +from wireup.ioc.util import parameter_get_type_and_annotation + + +def is_view_using_container(dependency_container: DependencyContainer, view: Callable[..., Any]) -> bool: + """Determine whether the view is using the given dependency container.""" + if hasattr(view, "__annotations__"): + for dep in inspect.signature(view).parameters.values(): + param = parameter_get_type_and_annotation(dep) + + is_requesting_injection = isinstance(param.annotation, InjectableType) + if is_requesting_injection or (param.klass and dependency_container.is_type_known(param.klass)): + return True + + return False