Skip to content

Commit

Permalink
Add fastapi integration
Browse files Browse the repository at this point in the history
  • Loading branch information
maldoinc committed Dec 22, 2023
1 parent 366f851 commit ae30191
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 16 deletions.
6 changes: 4 additions & 2 deletions test/integration/test_fastapi_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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("/")

Expand Down
42 changes: 42 additions & 0 deletions wireup/integration/fastapi_integration.py
Original file line number Diff line number Diff line change
@@ -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)
17 changes: 3 additions & 14 deletions wireup/integration/flask_integration.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
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

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],
Expand Down Expand Up @@ -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()
}
19 changes: 19 additions & 0 deletions wireup/integration/util.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit ae30191

Please sign in to comment.