mirror of https://github.com/tiangolo/fastapi.git
Merge branch 'master' into feature-branch-name
This commit is contained in:
commit
11056d3d75
|
|
@ -7,6 +7,24 @@ hide:
|
||||||
|
|
||||||
## Latest Changes
|
## Latest Changes
|
||||||
|
|
||||||
|
## 0.123.8
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* 🐛 Fix OpenAPI security scheme OAuth2 scopes declaration, deduplicate security schemes with different scopes. PR [#14455](https://github.com/fastapi/fastapi/pull/14455) by [@tiangolo](https://github.com/tiangolo).
|
||||||
|
|
||||||
|
## 0.123.7
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* 🐛 Fix evaluating stringified annotations in Python 3.10. PR [#11355](https://github.com/fastapi/fastapi/pull/11355) by [@chaen](https://github.com/chaen).
|
||||||
|
|
||||||
|
## 0.123.6
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* 🐛 Fix support for functools wraps and partial combined, for async and regular functions and classes in path operations and dependencies. PR [#14448](https://github.com/fastapi/fastapi/pull/14448) by [@tiangolo](https://github.com/tiangolo).
|
||||||
|
|
||||||
## 0.123.5
|
## 0.123.5
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||||
|
|
||||||
__version__ = "0.123.5"
|
__version__ = "0.123.8"
|
||||||
|
|
||||||
from starlette import status as status
|
from starlette import status as status
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,19 @@ else: # pragma: no cover
|
||||||
from asyncio import iscoroutinefunction
|
from asyncio import iscoroutinefunction
|
||||||
|
|
||||||
|
|
||||||
|
def _unwrapped_call(call: Optional[Callable[..., Any]]) -> Any:
|
||||||
|
if call is None:
|
||||||
|
return call # pragma: no cover
|
||||||
|
unwrapped = inspect.unwrap(_impartial(call))
|
||||||
|
return unwrapped
|
||||||
|
|
||||||
|
|
||||||
|
def _impartial(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
|
while isinstance(func, partial):
|
||||||
|
func = func.func
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SecurityRequirement:
|
class SecurityRequirement:
|
||||||
security_scheme: SecurityBase
|
security_scheme: SecurityBase
|
||||||
|
|
@ -75,37 +88,82 @@ 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
|
|
||||||
unwrapped = inspect.unwrap(self.call)
|
|
||||||
if isinstance(unwrapped, partial):
|
|
||||||
unwrapped = unwrapped.func
|
|
||||||
return unwrapped
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def is_gen_callable(self) -> bool:
|
def is_gen_callable(self) -> bool:
|
||||||
if inspect.isgeneratorfunction(self._unwrapped_call):
|
if self.call is None:
|
||||||
|
return False # pragma: no cover
|
||||||
|
if inspect.isgeneratorfunction(
|
||||||
|
_impartial(self.call)
|
||||||
|
) or inspect.isgeneratorfunction(_unwrapped_call(self.call)):
|
||||||
return True
|
return True
|
||||||
dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004
|
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
|
||||||
return inspect.isgeneratorfunction(dunder_call)
|
if dunder_call is None:
|
||||||
|
return False # pragma: no cover
|
||||||
|
if inspect.isgeneratorfunction(
|
||||||
|
_impartial(dunder_call)
|
||||||
|
) or inspect.isgeneratorfunction(_unwrapped_call(dunder_call)):
|
||||||
|
return True
|
||||||
|
dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004
|
||||||
|
if dunder_unwrapped_call is None:
|
||||||
|
return False # pragma: no cover
|
||||||
|
if inspect.isgeneratorfunction(
|
||||||
|
_impartial(dunder_unwrapped_call)
|
||||||
|
) or inspect.isgeneratorfunction(_unwrapped_call(dunder_unwrapped_call)):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def is_async_gen_callable(self) -> bool:
|
def is_async_gen_callable(self) -> bool:
|
||||||
if inspect.isasyncgenfunction(self._unwrapped_call):
|
if self.call is None:
|
||||||
|
return False # pragma: no cover
|
||||||
|
if inspect.isasyncgenfunction(
|
||||||
|
_impartial(self.call)
|
||||||
|
) or inspect.isasyncgenfunction(_unwrapped_call(self.call)):
|
||||||
return True
|
return True
|
||||||
dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004
|
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
|
||||||
return inspect.isasyncgenfunction(dunder_call)
|
if dunder_call is None:
|
||||||
|
return False # pragma: no cover
|
||||||
|
if inspect.isasyncgenfunction(
|
||||||
|
_impartial(dunder_call)
|
||||||
|
) or inspect.isasyncgenfunction(_unwrapped_call(dunder_call)):
|
||||||
|
return True
|
||||||
|
dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004
|
||||||
|
if dunder_unwrapped_call is None:
|
||||||
|
return False # pragma: no cover
|
||||||
|
if inspect.isasyncgenfunction(
|
||||||
|
_impartial(dunder_unwrapped_call)
|
||||||
|
) or inspect.isasyncgenfunction(_unwrapped_call(dunder_unwrapped_call)):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def is_coroutine_callable(self) -> bool:
|
def is_coroutine_callable(self) -> bool:
|
||||||
if inspect.isroutine(self._unwrapped_call):
|
if self.call is None:
|
||||||
return iscoroutinefunction(self._unwrapped_call)
|
return False # pragma: no cover
|
||||||
if inspect.isclass(self._unwrapped_call):
|
if inspect.isroutine(_impartial(self.call)) and iscoroutinefunction(
|
||||||
return False
|
_impartial(self.call)
|
||||||
dunder_call = getattr(self._unwrapped_call, "__call__", None) # noqa: B004
|
):
|
||||||
return iscoroutinefunction(dunder_call)
|
return True
|
||||||
|
if inspect.isroutine(_unwrapped_call(self.call)) and iscoroutinefunction(
|
||||||
|
_unwrapped_call(self.call)
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
|
||||||
|
if dunder_call is None:
|
||||||
|
return False # pragma: no cover
|
||||||
|
if iscoroutinefunction(_impartial(dunder_call)) or iscoroutinefunction(
|
||||||
|
_unwrapped_call(dunder_call)
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004
|
||||||
|
if dunder_unwrapped_call is None:
|
||||||
|
return False # pragma: no cover
|
||||||
|
if iscoroutinefunction(
|
||||||
|
_impartial(dunder_unwrapped_call)
|
||||||
|
) or iscoroutinefunction(_unwrapped_call(dunder_unwrapped_call)):
|
||||||
|
return True
|
||||||
|
# if inspect.isclass(self.call): False, covered by default return
|
||||||
|
return False
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def computed_scope(self) -> Union[str, None]:
|
def computed_scope(self) -> Union[str, None]:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import inspect
|
import inspect
|
||||||
|
import sys
|
||||||
from contextlib import AsyncExitStack, contextmanager
|
from contextlib import AsyncExitStack, contextmanager
|
||||||
from copy import copy, deepcopy
|
from copy import copy, deepcopy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
@ -191,7 +192,10 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]:
|
||||||
|
|
||||||
|
|
||||||
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
||||||
signature = inspect.signature(call)
|
if sys.version_info >= (3, 10):
|
||||||
|
signature = inspect.signature(call, eval_str=True)
|
||||||
|
else:
|
||||||
|
signature = inspect.signature(call)
|
||||||
unwrapped = inspect.unwrap(call)
|
unwrapped = inspect.unwrap(call)
|
||||||
globalns = getattr(unwrapped, "__globals__", {})
|
globalns = getattr(unwrapped, "__globals__", {})
|
||||||
typed_params = [
|
typed_params = [
|
||||||
|
|
@ -217,7 +221,10 @@ def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any:
|
||||||
|
|
||||||
|
|
||||||
def get_typed_return_annotation(call: Callable[..., Any]) -> Any:
|
def get_typed_return_annotation(call: Callable[..., Any]) -> Any:
|
||||||
signature = inspect.signature(call)
|
if sys.version_info >= (3, 10):
|
||||||
|
signature = inspect.signature(call, eval_str=True)
|
||||||
|
else:
|
||||||
|
signature = inspect.signature(call)
|
||||||
unwrapped = inspect.unwrap(call)
|
unwrapped = inspect.unwrap(call)
|
||||||
annotation = signature.return_annotation
|
annotation = signature.return_annotation
|
||||||
|
|
||||||
|
|
@ -548,10 +555,10 @@ async def _solve_generator(
|
||||||
*, dependant: Dependant, stack: AsyncExitStack, sub_values: Dict[str, Any]
|
*, dependant: Dependant, stack: AsyncExitStack, sub_values: Dict[str, Any]
|
||||||
) -> Any:
|
) -> Any:
|
||||||
assert dependant.call
|
assert dependant.call
|
||||||
if dependant.is_gen_callable:
|
if dependant.is_async_gen_callable:
|
||||||
cm = contextmanager_in_threadpool(contextmanager(dependant.call)(**sub_values))
|
|
||||||
elif dependant.is_async_gen_callable:
|
|
||||||
cm = asynccontextmanager(dependant.call)(**sub_values)
|
cm = asynccontextmanager(dependant.call)(**sub_values)
|
||||||
|
elif dependant.is_gen_callable:
|
||||||
|
cm = contextmanager_in_threadpool(contextmanager(dependant.call)(**sub_values))
|
||||||
return await stack.enter_async_context(cm)
|
return await stack.enter_async_context(cm)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,8 @@ def get_openapi_security_definitions(
|
||||||
flat_dependant: Dependant,
|
flat_dependant: Dependant,
|
||||||
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
|
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
|
||||||
security_definitions = {}
|
security_definitions = {}
|
||||||
operation_security = []
|
# Use a dict to merge scopes for same security scheme
|
||||||
|
operation_security_dict: Dict[str, List[str]] = {}
|
||||||
for security_requirement in flat_dependant.security_requirements:
|
for security_requirement in flat_dependant.security_requirements:
|
||||||
security_definition = jsonable_encoder(
|
security_definition = jsonable_encoder(
|
||||||
security_requirement.security_scheme.model,
|
security_requirement.security_scheme.model,
|
||||||
|
|
@ -88,7 +89,15 @@ def get_openapi_security_definitions(
|
||||||
)
|
)
|
||||||
security_name = security_requirement.security_scheme.scheme_name
|
security_name = security_requirement.security_scheme.scheme_name
|
||||||
security_definitions[security_name] = security_definition
|
security_definitions[security_name] = security_definition
|
||||||
operation_security.append({security_name: security_requirement.scopes})
|
# Merge scopes for the same security scheme
|
||||||
|
if security_name not in operation_security_dict:
|
||||||
|
operation_security_dict[security_name] = []
|
||||||
|
for scope in security_requirement.scopes or []:
|
||||||
|
if scope not in operation_security_dict[security_name]:
|
||||||
|
operation_security_dict[security_name].append(scope)
|
||||||
|
operation_security = [
|
||||||
|
{name: scopes} for name, scopes in operation_security_dict.items()
|
||||||
|
]
|
||||||
return security_definitions, operation_security
|
return security_definitions, operation_security
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
|
import inspect
|
||||||
|
import sys
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import AsyncGenerator, Generator
|
from typing import AsyncGenerator, Generator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi import Depends, FastAPI
|
from fastapi import Depends, FastAPI
|
||||||
|
from fastapi.concurrency import iterate_in_threadpool, run_in_threadpool
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 13): # pragma: no cover
|
||||||
|
from inspect import iscoroutinefunction
|
||||||
|
else: # pragma: no cover
|
||||||
|
from asyncio import iscoroutinefunction
|
||||||
|
|
||||||
|
|
||||||
def noop_wrap(func):
|
def noop_wrap(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
|
|
@ -14,8 +22,163 @@ def noop_wrap(func):
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def noop_wrap_async(func):
|
||||||
|
if inspect.isgeneratorfunction(func):
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
async def gen_wrapper(*args, **kwargs):
|
||||||
|
async for item in iterate_in_threadpool(func(*args, **kwargs)):
|
||||||
|
yield item
|
||||||
|
|
||||||
|
return gen_wrapper
|
||||||
|
|
||||||
|
elif inspect.isasyncgenfunction(func):
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
async def async_gen_wrapper(*args, **kwargs):
|
||||||
|
async for item in func(*args, **kwargs):
|
||||||
|
yield item
|
||||||
|
|
||||||
|
return async_gen_wrapper
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
if inspect.isroutine(func) and iscoroutinefunction(func):
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
if inspect.isclass(func):
|
||||||
|
return await run_in_threadpool(func, *args, **kwargs)
|
||||||
|
dunder_call = getattr(func, "__call__", None) # noqa: B004
|
||||||
|
if iscoroutinefunction(dunder_call):
|
||||||
|
return await dunder_call(*args, **kwargs)
|
||||||
|
return await run_in_threadpool(func, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class ClassInstanceDep:
|
||||||
|
def __call__(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class_instance_dep = ClassInstanceDep()
|
||||||
|
wrapped_class_instance_dep = noop_wrap(class_instance_dep)
|
||||||
|
wrapped_class_instance_dep_async_wrapper = noop_wrap_async(class_instance_dep)
|
||||||
|
|
||||||
|
|
||||||
|
class ClassInstanceGenDep:
|
||||||
|
def __call__(self):
|
||||||
|
yield True
|
||||||
|
|
||||||
|
|
||||||
|
class_instance_gen_dep = ClassInstanceGenDep()
|
||||||
|
wrapped_class_instance_gen_dep = noop_wrap(class_instance_gen_dep)
|
||||||
|
|
||||||
|
|
||||||
|
class ClassInstanceWrappedDep:
|
||||||
|
@noop_wrap
|
||||||
|
def __call__(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class_instance_wrapped_dep = ClassInstanceWrappedDep()
|
||||||
|
|
||||||
|
|
||||||
|
class ClassInstanceWrappedAsyncDep:
|
||||||
|
@noop_wrap_async
|
||||||
|
def __call__(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class_instance_wrapped_async_dep = ClassInstanceWrappedAsyncDep()
|
||||||
|
|
||||||
|
|
||||||
|
class ClassInstanceWrappedGenDep:
|
||||||
|
@noop_wrap
|
||||||
|
def __call__(self):
|
||||||
|
yield True
|
||||||
|
|
||||||
|
|
||||||
|
class_instance_wrapped_gen_dep = ClassInstanceWrappedGenDep()
|
||||||
|
|
||||||
|
|
||||||
|
class ClassInstanceWrappedAsyncGenDep:
|
||||||
|
@noop_wrap_async
|
||||||
|
def __call__(self):
|
||||||
|
yield True
|
||||||
|
|
||||||
|
|
||||||
|
class_instance_wrapped_async_gen_dep = ClassInstanceWrappedAsyncGenDep()
|
||||||
|
|
||||||
|
|
||||||
|
class ClassDep:
|
||||||
|
def __init__(self):
|
||||||
|
self.value = True
|
||||||
|
|
||||||
|
|
||||||
|
wrapped_class_dep = noop_wrap(ClassDep)
|
||||||
|
wrapped_class_dep_async_wrapper = noop_wrap_async(ClassDep)
|
||||||
|
|
||||||
|
|
||||||
|
class ClassInstanceAsyncDep:
|
||||||
|
async def __call__(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class_instance_async_dep = ClassInstanceAsyncDep()
|
||||||
|
wrapped_class_instance_async_dep = noop_wrap(class_instance_async_dep)
|
||||||
|
wrapped_class_instance_async_dep_async_wrapper = noop_wrap_async(
|
||||||
|
class_instance_async_dep
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ClassInstanceAsyncGenDep:
|
||||||
|
async def __call__(self):
|
||||||
|
yield True
|
||||||
|
|
||||||
|
|
||||||
|
class_instance_async_gen_dep = ClassInstanceAsyncGenDep()
|
||||||
|
wrapped_class_instance_async_gen_dep = noop_wrap(class_instance_async_gen_dep)
|
||||||
|
|
||||||
|
|
||||||
|
class ClassInstanceAsyncWrappedDep:
|
||||||
|
@noop_wrap
|
||||||
|
async def __call__(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class_instance_async_wrapped_dep = ClassInstanceAsyncWrappedDep()
|
||||||
|
|
||||||
|
|
||||||
|
class ClassInstanceAsyncWrappedAsyncDep:
|
||||||
|
@noop_wrap_async
|
||||||
|
async def __call__(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class_instance_async_wrapped_async_dep = ClassInstanceAsyncWrappedAsyncDep()
|
||||||
|
|
||||||
|
|
||||||
|
class ClassInstanceAsyncWrappedGenDep:
|
||||||
|
@noop_wrap
|
||||||
|
async def __call__(self):
|
||||||
|
yield True
|
||||||
|
|
||||||
|
|
||||||
|
class_instance_async_wrapped_gen_dep = ClassInstanceAsyncWrappedGenDep()
|
||||||
|
|
||||||
|
|
||||||
|
class ClassInstanceAsyncWrappedGenAsyncDep:
|
||||||
|
@noop_wrap_async
|
||||||
|
async def __call__(self):
|
||||||
|
yield True
|
||||||
|
|
||||||
|
|
||||||
|
class_instance_async_wrapped_gen_async_dep = ClassInstanceAsyncWrappedGenAsyncDep()
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
|
# Sync wrapper
|
||||||
|
|
||||||
|
|
||||||
@noop_wrap
|
@noop_wrap
|
||||||
def wrapped_dependency() -> bool:
|
def wrapped_dependency() -> bool:
|
||||||
|
|
@ -59,16 +222,225 @@ async def get_async_wrapped_gen_dependency(
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/wrapped-class-instance-dependency/")
|
||||||
|
async def get_wrapped_class_instance_dependency(
|
||||||
|
value: bool = Depends(wrapped_class_instance_dep),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/wrapped-class-instance-async-dependency/")
|
||||||
|
async def get_wrapped_class_instance_async_dependency(
|
||||||
|
value: bool = Depends(wrapped_class_instance_async_dep),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/wrapped-class-instance-gen-dependency/")
|
||||||
|
async def get_wrapped_class_instance_gen_dependency(
|
||||||
|
value: bool = Depends(wrapped_class_instance_gen_dep),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/wrapped-class-instance-async-gen-dependency/")
|
||||||
|
async def get_wrapped_class_instance_async_gen_dependency(
|
||||||
|
value: bool = Depends(wrapped_class_instance_async_gen_dep),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/class-instance-wrapped-dependency/")
|
||||||
|
async def get_class_instance_wrapped_dependency(
|
||||||
|
value: bool = Depends(class_instance_wrapped_dep),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/class-instance-wrapped-async-dependency/")
|
||||||
|
async def get_class_instance_wrapped_async_dependency(
|
||||||
|
value: bool = Depends(class_instance_wrapped_async_dep),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/class-instance-async-wrapped-dependency/")
|
||||||
|
async def get_class_instance_async_wrapped_dependency(
|
||||||
|
value: bool = Depends(class_instance_async_wrapped_dep),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/class-instance-async-wrapped-async-dependency/")
|
||||||
|
async def get_class_instance_async_wrapped_async_dependency(
|
||||||
|
value: bool = Depends(class_instance_async_wrapped_async_dep),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/class-instance-wrapped-gen-dependency/")
|
||||||
|
async def get_class_instance_wrapped_gen_dependency(
|
||||||
|
value: bool = Depends(class_instance_wrapped_gen_dep),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/class-instance-wrapped-async-gen-dependency/")
|
||||||
|
async def get_class_instance_wrapped_async_gen_dependency(
|
||||||
|
value: bool = Depends(class_instance_wrapped_async_gen_dep),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/class-instance-async-wrapped-gen-dependency/")
|
||||||
|
async def get_class_instance_async_wrapped_gen_dependency(
|
||||||
|
value: bool = Depends(class_instance_async_wrapped_gen_dep),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/class-instance-async-wrapped-gen-async-dependency/")
|
||||||
|
async def get_class_instance_async_wrapped_gen_async_dependency(
|
||||||
|
value: bool = Depends(class_instance_async_wrapped_gen_async_dep),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/wrapped-class-dependency/")
|
||||||
|
async def get_wrapped_class_dependency(value: ClassDep = Depends(wrapped_class_dep)):
|
||||||
|
return value.value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/wrapped-endpoint/")
|
||||||
|
@noop_wrap
|
||||||
|
def get_wrapped_endpoint():
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/async-wrapped-endpoint/")
|
||||||
|
@noop_wrap
|
||||||
|
async def get_async_wrapped_endpoint():
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# Async wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@noop_wrap_async
|
||||||
|
def wrapped_dependency_async_wrapper() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@noop_wrap_async
|
||||||
|
def wrapped_gen_dependency_async_wrapper() -> Generator[bool, None, None]:
|
||||||
|
yield True
|
||||||
|
|
||||||
|
|
||||||
|
@noop_wrap_async
|
||||||
|
async def async_wrapped_dependency_async_wrapper() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@noop_wrap_async
|
||||||
|
async def async_wrapped_gen_dependency_async_wrapper() -> AsyncGenerator[bool, None]:
|
||||||
|
yield True
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/wrapped-dependency-async-wrapper/")
|
||||||
|
async def get_wrapped_dependency_async_wrapper(
|
||||||
|
value: bool = Depends(wrapped_dependency_async_wrapper),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/wrapped-gen-dependency-async-wrapper/")
|
||||||
|
async def get_wrapped_gen_dependency_async_wrapper(
|
||||||
|
value: bool = Depends(wrapped_gen_dependency_async_wrapper),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/async-wrapped-dependency-async-wrapper/")
|
||||||
|
async def get_async_wrapped_dependency_async_wrapper(
|
||||||
|
value: bool = Depends(async_wrapped_dependency_async_wrapper),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/async-wrapped-gen-dependency-async-wrapper/")
|
||||||
|
async def get_async_wrapped_gen_dependency_async_wrapper(
|
||||||
|
value: bool = Depends(async_wrapped_gen_dependency_async_wrapper),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/wrapped-class-instance-dependency-async-wrapper/")
|
||||||
|
async def get_wrapped_class_instance_dependency_async_wrapper(
|
||||||
|
value: bool = Depends(wrapped_class_instance_dep_async_wrapper),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/wrapped-class-instance-async-dependency-async-wrapper/")
|
||||||
|
async def get_wrapped_class_instance_async_dependency_async_wrapper(
|
||||||
|
value: bool = Depends(wrapped_class_instance_async_dep_async_wrapper),
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/wrapped-class-dependency-async-wrapper/")
|
||||||
|
async def get_wrapped_class_dependency_async_wrapper(
|
||||||
|
value: ClassDep = Depends(wrapped_class_dep_async_wrapper),
|
||||||
|
):
|
||||||
|
return value.value
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/wrapped-endpoint-async-wrapper/")
|
||||||
|
@noop_wrap_async
|
||||||
|
def get_wrapped_endpoint_async_wrapper():
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/async-wrapped-endpoint-async-wrapper/")
|
||||||
|
@noop_wrap_async
|
||||||
|
async def get_async_wrapped_endpoint_async_wrapper():
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"route",
|
"route",
|
||||||
[
|
[
|
||||||
"/wrapped-dependency",
|
"/wrapped-dependency/",
|
||||||
"/wrapped-gen-dependency",
|
"/wrapped-gen-dependency/",
|
||||||
"/async-wrapped-dependency",
|
"/async-wrapped-dependency/",
|
||||||
"/async-wrapped-gen-dependency",
|
"/async-wrapped-gen-dependency/",
|
||||||
|
"/wrapped-class-instance-dependency/",
|
||||||
|
"/wrapped-class-instance-async-dependency/",
|
||||||
|
"/wrapped-class-instance-gen-dependency/",
|
||||||
|
"/wrapped-class-instance-async-gen-dependency/",
|
||||||
|
"/class-instance-wrapped-dependency/",
|
||||||
|
"/class-instance-wrapped-async-dependency/",
|
||||||
|
"/class-instance-async-wrapped-dependency/",
|
||||||
|
"/class-instance-async-wrapped-async-dependency/",
|
||||||
|
"/class-instance-wrapped-gen-dependency/",
|
||||||
|
"/class-instance-wrapped-async-gen-dependency/",
|
||||||
|
"/class-instance-async-wrapped-gen-dependency/",
|
||||||
|
"/class-instance-async-wrapped-gen-async-dependency/",
|
||||||
|
"/wrapped-class-dependency/",
|
||||||
|
"/wrapped-endpoint/",
|
||||||
|
"/async-wrapped-endpoint/",
|
||||||
|
"/wrapped-dependency-async-wrapper/",
|
||||||
|
"/wrapped-gen-dependency-async-wrapper/",
|
||||||
|
"/async-wrapped-dependency-async-wrapper/",
|
||||||
|
"/async-wrapped-gen-dependency-async-wrapper/",
|
||||||
|
"/wrapped-class-instance-dependency-async-wrapper/",
|
||||||
|
"/wrapped-class-instance-async-dependency-async-wrapper/",
|
||||||
|
"/wrapped-class-dependency-async-wrapper/",
|
||||||
|
"/wrapped-endpoint-async-wrapper/",
|
||||||
|
"/async-wrapped-endpoint-async-wrapper/",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_class_dependency(route):
|
def test_class_dependency(route):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
# Ref: https://github.com/fastapi/fastapi/issues/14454
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, FastAPI, Security
|
||||||
|
from fastapi.security import OAuth2AuthorizationCodeBearer
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from inline_snapshot import snapshot
|
||||||
|
|
||||||
|
oauth2_scheme = OAuth2AuthorizationCodeBearer(
|
||||||
|
authorizationUrl="authorize",
|
||||||
|
tokenUrl="token",
|
||||||
|
auto_error=True,
|
||||||
|
scopes={"read": "Read access", "write": "Write access"},
|
||||||
|
)
|
||||||
|
|
||||||
|
app = FastAPI(dependencies=[Security(oauth2_scheme)])
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
return {"message": "Hello World"}
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(dependencies=[Security(oauth2_scheme, scopes=["read"])])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/items/")
|
||||||
|
async def read_items(token: Optional[str] = Security(oauth2_scheme)):
|
||||||
|
return {"token": token}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/items/")
|
||||||
|
async def create_item(
|
||||||
|
token: Optional[str] = Security(oauth2_scheme, scopes=["read", "write"]),
|
||||||
|
):
|
||||||
|
return {"token": token}
|
||||||
|
|
||||||
|
|
||||||
|
app.include_router(router)
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_root():
|
||||||
|
response = client.get("/", headers={"Authorization": "Bearer testtoken"})
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == {"message": "Hello World"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_token():
|
||||||
|
response = client.get("/items/", headers={"Authorization": "Bearer testtoken"})
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == {"token": "testtoken"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_token():
|
||||||
|
response = client.post("/items/", headers={"Authorization": "Bearer testtoken"})
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == {"token": "testtoken"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_openapi_schema():
|
||||||
|
response = client.get("/openapi.json")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == snapshot(
|
||||||
|
{
|
||||||
|
"openapi": "3.1.0",
|
||||||
|
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||||
|
"paths": {
|
||||||
|
"/": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Root",
|
||||||
|
"operationId": "root__get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [{"OAuth2AuthorizationCodeBearer": []}],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/items/": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Read Items",
|
||||||
|
"operationId": "read_items_items__get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{"OAuth2AuthorizationCodeBearer": ["read"]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"summary": "Create Item",
|
||||||
|
"operationId": "create_item_items__post",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{"OAuth2AuthorizationCodeBearer": ["read", "write"]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"securitySchemes": {
|
||||||
|
"OAuth2AuthorizationCodeBearer": {
|
||||||
|
"type": "oauth2",
|
||||||
|
"flows": {
|
||||||
|
"authorizationCode": {
|
||||||
|
"scopes": {
|
||||||
|
"read": "Read access",
|
||||||
|
"write": "Write access",
|
||||||
|
},
|
||||||
|
"authorizationUrl": "authorize",
|
||||||
|
"tokenUrl": "token",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import Depends, FastAPI, Request
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
from .utils import needs_py310
|
||||||
|
|
||||||
|
|
||||||
|
class Dep:
|
||||||
|
def __call__(self, request: Request):
|
||||||
|
return "test"
|
||||||
|
|
||||||
|
|
||||||
|
@needs_py310
|
||||||
|
def test_stringified_annotations():
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
@app.get("/test/")
|
||||||
|
def call(test: Annotated[str, Depends(Dep())]):
|
||||||
|
return {"test": test}
|
||||||
|
|
||||||
|
response = client.get("/test")
|
||||||
|
assert response.status_code == 200
|
||||||
Loading…
Reference in New Issue