Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Wireup in fastapi aliases #43

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions docs/pages/integrations/fastapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Dependency injection for FastAPI is available in the `wireup.integration.fastapi
- [x] Expose `fastapi.Request` as a wireup dependency.
* Available as a `TRANSIENT` scoped dependency, your services can ask for a fastapi request object.
- [x] Can: Mix Wireup and FastAPI dependencies in routes.
- [x] Can: Use Wireup in FastAPI dependencies.
- [ ] Cannot: Use FastAPI dependencies in Wireup service objects.

## Getting Started
Expand Down Expand Up @@ -39,9 +40,14 @@ container = wireup.create_container(
wireup.integration.fastapi.setup(container, app)
```

Wireup integration performs injection only in fastapi routes. If you're not storing the container in a global variable,
you can always get a reference to it wherever you have a fastapi application reference
by using `wireup.integration.fastapi.get_container`.
## Use Wireup in FastAPI Depends

To help with migration, it is possible to use Wireup in FastAPI depends
or anywhere you have a reference to the FastAPI application instance.


If you're not storing the container in a variable somewhere, you can get a reference
to it by using the `wireup.integration.fastapi.get_container` function.

```python title="example_middleware.py"
from wireup.integration.fastapi import get_container
Expand All @@ -58,11 +64,23 @@ In the same way, you can get a reference to it in a fastapi dependency.
```python
from wireup.integration.fastapi import get_container

async def example_dependency(request: Request, other_dependency: Depends(...)):
async def example_dependency(request: Request, other_dependency: Annotated[X, Depends(...)]):
container = get_container(request.app)
...
```

The integration also provides some FastAPI Depends functions you can use directly that perform some operation
with the container.

```python
async def some_fastapi_dependency(
greeter: Annotated[GreeterService, WireupService(GreeterService)],
foo_param: Annotated[str, WireupParameter("foo")],
foo_foo: Annotated[str, WireupExpr("${foo}-${foo}")],
container: Annotated[DependencyContainer, WireupContainer()],
): ...
```

### FastAPI request

A key feature of the integration is to expose `fastapi.Request` and `starlette.requests.Request` objects in wireup.
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ lint.ignore = [
"D213", # Disable "Summary must go into next line"
"D107", # Disable required docs for __init. Can be redundant if class also has them.
"A003", # Disable "shadows builtin". OverrideManager.set was flagged by this
"N802", # Disable due to various functions used in Annotated[].
"FA100", # Don't recomment __future__ annotations.
# Disable as they may cause conflicts with ruff formatter
"COM812",
Expand Down
29 changes: 28 additions & 1 deletion test/integration/test_fastapi_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
from typing_extensions import Annotated
from wireup import Inject
from wireup.errors import UnknownServiceRequestedError, WireupError
from wireup.integration.fastapi import get_container
from wireup.integration.fastapi import WireupContainer, WireupExpr, WireupParameter, WireupService, get_container
from wireup.ioc.dependency_container import DependencyContainer
from wireup.ioc.types import ServiceLifetime

from test.unit.services.no_annotations.random.random_service import RandomService
Expand All @@ -40,6 +41,13 @@ def greet(self, name: str) -> str:
return f"Hello {name}"


def get_greeting(
greeter: Annotated[GreeterService, WireupService(GreeterService)],
name: str,
) -> str:
return greeter.greet(name)


def create_app() -> FastAPI:
app = FastAPI()

Expand All @@ -53,6 +61,19 @@ async def lucky_number_route(
async def rng_route(random_service: Annotated[RandomService, Inject()]):
return {"number": random_service.get_random()}

@app.get("/wireup-in-fastapi")
async def wireup_in_fastapi_depends(
greeting: Annotated[str, Depends(get_greeting)],
foo_param: Annotated[str, WireupParameter("foo")],
foo_foo: Annotated[str, WireupExpr("${foo}-${foo}")],
container: Annotated[DependencyContainer, WireupContainer()],
):
assert foo_param == "bar"
assert foo_foo == "bar-bar"
assert isinstance(container, DependencyContainer)

return {"greeting": greeting}

@app.get("/params")
async def params_route(
foo: Annotated[str, Inject(param="foo")], foo_foo: Annotated[str, Inject(expr="${foo}-${foo}")]
Expand Down Expand Up @@ -99,6 +120,12 @@ def test_injects_service(client: TestClient):
assert response.json() == {"number": 4, "lucky_number": 42}


def test_injects_wireup_in_fastapi_depends(client: TestClient):
response = client.get("/wireup-in-fastapi", params={"name": "World"})
assert response.status_code == 200
assert response.json() == {"greeting": "Hello World"}


def test_override(app: FastAPI, client: TestClient):
class RealRandom(RandomService):
def get_random(self) -> int:
Expand Down
2 changes: 1 addition & 1 deletion wireup/annotation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from collections.abc import Callable


def Inject( # noqa: N802
def Inject(
*,
param: str | None = None,
expr: str | None = None,
Expand Down
44 changes: 40 additions & 4 deletions wireup/integration/fastapi.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,52 @@
from __future__ import annotations

from contextvars import ContextVar
from typing import Awaitable, Callable
from typing import TYPE_CHECKING, Any, Awaitable, Callable, TypeVar

from fastapi import FastAPI, Request, Response
from fastapi import Depends, FastAPI, Request, Response
from fastapi.routing import APIRoute, APIWebSocketRoute

from wireup import DependencyContainer
from wireup.errors import WireupError
from wireup.integration.util import is_view_using_container
from wireup.ioc.types import ServiceLifetime
from wireup.ioc.types import Qualifier, ServiceLifetime, TemplatedString

if TYPE_CHECKING:
from wireup import DependencyContainer

current_request: ContextVar[Request] = ContextVar("wireup_fastapi_request")

T = TypeVar("T")


def WireupContainer() -> Callable[[], DependencyContainer]:
"""Inject the wireup container associated with the current FastAPI instance."""

def _depends(request: Request) -> DependencyContainer:
return get_container(request.app)

return Depends(_depends)


def WireupService(service: type[T], qualifier: Qualifier | None = None) -> Callable[..., T]: # noqa: D103
def _depends(request: Request) -> T:
return get_container(request.app).get(service, qualifier)

return Depends(_depends)


def WireupParameter(param: str) -> Callable[..., Any]: # noqa: D103
def _depends(request: Request) -> Any:
return get_container(request.app).params.get(param)

return Depends(_depends)


def WireupExpr(expr: str) -> Callable[..., Any]: # noqa: D103
def _depends(request: Request) -> Any:
return get_container(request.app).params.get(TemplatedString(expr))

return Depends(_depends)


async def _wireup_request_middleware(request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
token = current_request.set(request)
Expand Down
Loading