Browse Source

🔄 Merge branch 'master' into pr/lucaswiman/5077

pull/5077/head
Sebastián Ramírez 8 months ago
parent
commit
4cac52b371
  1. 9
      docs/en/docs/release-notes.md
  2. 22
      fastapi/dependencies/models.py
  3. 8
      fastapi/routing.py
  4. 77
      tests/test_dependency_wrapped.py

9
docs/en/docs/release-notes.md

@ -7,6 +7,15 @@ hide:
## Latest Changes ## Latest Changes
### Features
* ✨ Handle wrapped dependencies. PR [#9555](https://github.com/fastapi/fastapi/pull/9555) by [@phy1729](https://github.com/phy1729).
### Refactors
* 🔥 Remove dangling extra condiitonal no longer needed. PR [#14435](https://github.com/fastapi/fastapi/pull/14435) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Refactor internals, update `is_coroutine` check to reuse internal supported variants (unwrap, check class). PR [#14434](https://github.com/fastapi/fastapi/pull/14434) by [@tiangolo](https://github.com/tiangolo).
## 0.123.4 ## 0.123.4
### Fixes ### Fixes

22
fastapi/dependencies/models.py

@ -75,27 +75,33 @@ class Dependant:
return True return True
return False return False
@cached_property
def _unwrapped_call(self) -> Any:
if self.call is None:
return self.call # pragma: no cover
return inspect.unwrap(self.call)
@cached_property @cached_property
def is_gen_callable(self) -> bool: def is_gen_callable(self) -> bool:
if inspect.isgeneratorfunction(self.call): if inspect.isgeneratorfunction(self._unwrapped_call):
return True return True
dunder_call = getattr(self.call, "__call__", None) # noqa: B004 dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004
return inspect.isgeneratorfunction(dunder_call) return inspect.isgeneratorfunction(dunder_call)
@cached_property @cached_property
def is_async_gen_callable(self) -> bool: def is_async_gen_callable(self) -> bool:
if inspect.isasyncgenfunction(self.call): if inspect.isasyncgenfunction(self._unwrapped_call):
return True return True
dunder_call = getattr(self.call, "__call__", None) # noqa: B004 dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004
return inspect.isasyncgenfunction(dunder_call) return inspect.isasyncgenfunction(dunder_call)
@cached_property @cached_property
def is_coroutine_callable(self) -> bool: def is_coroutine_callable(self) -> bool:
if inspect.isroutine(self.call): if inspect.isroutine(self._unwrapped_call):
return iscoroutinefunction(self.call) return iscoroutinefunction(self._unwrapped_call)
if inspect.isclass(self.call): if inspect.isclass(self._unwrapped_call):
return False return False
dunder_call = getattr(self.call, "__call__", None) # noqa: B004 dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004
return iscoroutinefunction(dunder_call) return iscoroutinefunction(dunder_call)
@cached_property @cached_property

8
fastapi/routing.py

@ -3,7 +3,6 @@ import email.message
import functools import functools
import inspect import inspect
import json import json
import sys
from contextlib import AsyncExitStack, asynccontextmanager from contextlib import AsyncExitStack, asynccontextmanager
from enum import Enum, IntEnum from enum import Enum, IntEnum
from typing import ( from typing import (
@ -79,11 +78,6 @@ from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send
from starlette.websockets import WebSocket from starlette.websockets import WebSocket
from typing_extensions import Annotated, deprecated from typing_extensions import Annotated, deprecated
if sys.version_info >= (3, 13): # pragma: no cover
from inspect import iscoroutinefunction
else: # pragma: no cover
from asyncio import iscoroutinefunction
# Copy of starlette.routing.request_response modified to include the # Copy of starlette.routing.request_response modified to include the
# dependencies' AsyncExitStack # dependencies' AsyncExitStack
@ -308,7 +302,7 @@ def get_request_handler(
embed_body_fields: bool = False, embed_body_fields: bool = False,
) -> Callable[[Request], Coroutine[Any, Any, Response]]: ) -> Callable[[Request], Coroutine[Any, Any, Response]]:
assert dependant.call is not None, "dependant.call must be a function" assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = iscoroutinefunction(dependant.call) is_coroutine = dependant.is_coroutine_callable
is_body_form = body_field and isinstance( is_body_form = body_field and isinstance(
body_field.field_info, (params.Form, temp_pydantic_v1_params.Form) body_field.field_info, (params.Form, temp_pydantic_v1_params.Form)
) )

77
tests/test_dependency_wrapped.py

@ -0,0 +1,77 @@
from functools import wraps
from typing import AsyncGenerator, Generator
import pytest
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
def noop_wrap(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
app = FastAPI()
@noop_wrap
def wrapped_dependency() -> bool:
return True
@noop_wrap
def wrapped_gen_dependency() -> Generator[bool, None, None]:
yield True
@noop_wrap
async def async_wrapped_dependency() -> bool:
return True
@noop_wrap
async def async_wrapped_gen_dependency() -> AsyncGenerator[bool, None]:
yield True
@app.get("/wrapped-dependency/")
async def get_wrapped_dependency(value: bool = Depends(wrapped_dependency)):
return value
@app.get("/wrapped-gen-dependency/")
async def get_wrapped_gen_dependency(value: bool = Depends(wrapped_gen_dependency)):
return value
@app.get("/async-wrapped-dependency/")
async def get_async_wrapped_dependency(value: bool = Depends(async_wrapped_dependency)):
return value
@app.get("/async-wrapped-gen-dependency/")
async def get_async_wrapped_gen_dependency(
value: bool = Depends(async_wrapped_gen_dependency),
):
return value
client = TestClient(app)
@pytest.mark.parametrize(
"route",
[
"/wrapped-dependency",
"/wrapped-gen-dependency",
"/async-wrapped-dependency",
"/async-wrapped-gen-dependency",
],
)
def test_class_dependency(route):
response = client.get(route)
assert response.status_code == 200, response.text
assert response.json() is True
Loading…
Cancel
Save