Skip to content

Commit

Permalink
Add fastapi integration (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
maldoinc committed Dec 28, 2023
1 parent 381f94e commit eaeabfe
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 24 deletions.
5 changes: 4 additions & 1 deletion docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ nav:
- Working with Interfaces: interfaces.md
- Manual configuration: manual_configuration.md
- Multiple containers: multiple_containers.md
- Integrations:
- Flask: integrations/flask.md
- FastAPI: integrations/fastapi.md
- Misc:
- Flask Integration: flask_integration.md
- Introduce to an existing project: introduce_to_an_existing_project.md
- Demo application: demo_app.md
- Versioning: versioning.md
Expand All @@ -24,6 +26,7 @@ nav:
- ParameterEnum: class/parameter_enum.md
- InitializationContext: class/initialization_context.md
- flask_integration: class/flask_integration.md
- fastapi_integration: class/fastapi_integration.md
repo_url: https://github.com/maldoinc/wireup
repo_name: maldoinc/wireup
theme:
Expand Down
3 changes: 3 additions & 0 deletions docs/pages/class/fastapi_integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## wireup_init_fastapi_integration
::: wireup.integration.fastapi_integration.wireup_init_fastapi_integration

5 changes: 2 additions & 3 deletions docs/pages/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
Dependency injection container with a focus on developer experience, type safety and ease of use.

!!! note "New: Dependency injection for Flask"
Simplify usage in Flask applications by using the new [Flask integration](flask_integration.md)!
Simplify usage in Flask applications by using the new [Flask integration](integrations/flask)!

* Automatically inject dependencies on views without having to manually decorate.
* Expose flask application configuration in the container.
* Expose Flask application configuration in the container.

## Key features

Expand Down Expand Up @@ -39,7 +39,6 @@ Dependency injection container with a focus on developer experience, type safety
Interfaces / Abstract classes
</div>
Define abstract types and have the container automatically inject the implementation.
Say goodbye to mocks in your tests!
</div>
<div class="card">
<div class="card-title">
Expand Down
51 changes: 51 additions & 0 deletions docs/pages/integrations/fastapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
Dependency injection for FastAPI (all versions) is available via the first-party integration wireup provides, available in
`wireup.integration.fastapi_integration`.


**Features:**

* Automatically decorate Flask views and blueprints where the container is being used.
* Eliminates the need for `@container.autowire` in views.
* Views without container references will not be decorated.
* Services **must** be annotated with `Wire()`.
* Can: Mix FastAPI dependencies and Wireup in views
* Can: Autowire any FastAPI target with `@container.autowire`.
* Cannot: Use FastAPI dependencies in Wireup service objects.

!!! tip
As FastAPI does not have a fixed configuration mechanism, you need to expose
any configuration objects to the container using one of the two options:

* By dumping all values in the parameter bag.
* Registering the configuration object as a service using a factory function.

## Examples

```python

app = FastAPI()

@app.get("/random")
async def target(
# Wire annotation tells wireup that this argument should be injected.
random_service: Annotated[RandomService, Wire()],
is_debug: Annotated[bool, Wire(param="env.debug")],

# This is a regular FastAPI dependency.
lucky_number: Annotated[int, Depends(get_lucky_number)]
):
return {
"number": random_service.get_random(),
"lucky_number": lucky_number,
"is_debug": is_debug,
}

# Initialize the integration.
# Must be called after all views have been registered.
# Pass to service_modules a list of top-level modules where your services reside.
wireup_init_fastapi_integration(app, service_modules=[services])
```

## Api Reference

* [fastapi_integration](../class/fastapi_integration.md)
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ if __name__ == '__main__':
### Using parameter enums

Parameter enums offer a typed way of representing parameters.
See [Parameter Enum documentation for more details](parameters.md#parameter-enums)
See [Parameter Enum documentation for more details](../parameters.md#parameter-enums)

## Api Reference

* [flask_integration](class/flask_integration.md)
* [ParameterEnum](class/parameter_enum.md)
* [flask_integration](../class/flask_integration.md)
* [ParameterEnum](../class/parameter_enum.md)
2 changes: 1 addition & 1 deletion docs/pages/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def get_posts(post_repository: PostRepository):
```

1. Decorate all methods where the library must perform injection.
Optional when using the [Flask integration](flask_integration.md).
Optional when using the [Flask integration](integrations/flask).


**Installation**
Expand Down
5 changes: 3 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 @@ -22,22 +23,22 @@ def get_lucky_number() -> int:
return 42

@self.app.get("/")
@self.container.autowire
async def target(
random_service: Annotated[RandomService, Wire()], lucky_number: Annotated[int, Depends(get_lucky_number)]
):
return {"number": random_service.get_random(), "lucky_number": lucky_number}

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, "lucky_number": 42})

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.dependant.call)
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()
}
18 changes: 18 additions & 0 deletions wireup/integration/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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."""
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 eaeabfe

Please sign in to comment.