mirror of https://github.com/tiangolo/fastapi.git
✨Add support for PEP-593 `Annotated` for specifying dependencies and parameters (#4871)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
This commit is contained in:
parent
ef176c6631
commit
375513f114
|
|
@ -0,0 +1,18 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Depends, FastAPI
|
||||||
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100):
|
||||||
|
return {"q": q, "skip": skip, "limit": limit}
|
||||||
|
|
||||||
|
|
||||||
|
CommonParamsDepends = Annotated[dict, Depends(common_parameters)]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/items/")
|
||||||
|
async def read_items(commons: CommonParamsDepends):
|
||||||
|
return commons
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from typing import Annotated, Optional
|
||||||
|
|
||||||
|
from fastapi import Depends, FastAPI
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100):
|
||||||
|
return {"q": q, "skip": skip, "limit": limit}
|
||||||
|
|
||||||
|
|
||||||
|
CommonParamsDepends = Annotated[dict, Depends(common_parameters)]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/items/")
|
||||||
|
async def read_items(commons: CommonParamsDepends):
|
||||||
|
return commons
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Depends, FastAPI
|
||||||
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
class CommonQueryParams:
|
||||||
|
def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100):
|
||||||
|
self.q = q
|
||||||
|
self.skip = skip
|
||||||
|
self.limit = limit
|
||||||
|
|
||||||
|
|
||||||
|
CommonQueryParamsDepends = Annotated[CommonQueryParams, Depends()]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/items/")
|
||||||
|
async def read_items(commons: CommonQueryParamsDepends):
|
||||||
|
return commons
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
from typing import Annotated, Optional
|
||||||
|
|
||||||
|
from fastapi import Depends, FastAPI
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
class CommonQueryParams:
|
||||||
|
def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100):
|
||||||
|
self.q = q
|
||||||
|
self.skip = skip
|
||||||
|
self.limit = limit
|
||||||
|
|
||||||
|
|
||||||
|
CommonQueryParamsDepends = Annotated[CommonQueryParams, Depends()]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/items/")
|
||||||
|
async def read_items(commons: CommonQueryParamsDepends):
|
||||||
|
return commons
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from fastapi import FastAPI, Path
|
||||||
|
from fastapi.param_functions import Query
|
||||||
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/items/{item_id}")
|
||||||
|
async def read_items(item_id: Annotated[int, Path(gt=0)]):
|
||||||
|
return {"item_id": item_id}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/users")
|
||||||
|
async def read_users(user_id: Annotated[str, Query(min_length=1)] = "me"):
|
||||||
|
return {"user_id": user_id}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Path
|
||||||
|
from fastapi.param_functions import Query
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/items/{item_id}")
|
||||||
|
async def read_items(item_id: Annotated[int, Path(gt=0)]):
|
||||||
|
return {"item_id": item_id}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/users")
|
||||||
|
async def read_users(user_id: Annotated[str, Query(min_length=1)] = "me"):
|
||||||
|
return {"user_id": user_id}
|
||||||
|
|
@ -48,7 +48,7 @@ from pydantic.fields import (
|
||||||
Undefined,
|
Undefined,
|
||||||
)
|
)
|
||||||
from pydantic.schema import get_annotation_from_field_info
|
from pydantic.schema import get_annotation_from_field_info
|
||||||
from pydantic.typing import evaluate_forwardref
|
from pydantic.typing import evaluate_forwardref, get_args, get_origin
|
||||||
from pydantic.utils import lenient_issubclass
|
from pydantic.utils import lenient_issubclass
|
||||||
from starlette.background import BackgroundTasks
|
from starlette.background import BackgroundTasks
|
||||||
from starlette.concurrency import run_in_threadpool
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
@ -56,6 +56,7 @@ from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
|
||||||
from starlette.requests import HTTPConnection, Request
|
from starlette.requests import HTTPConnection, Request
|
||||||
from starlette.responses import Response
|
from starlette.responses import Response
|
||||||
from starlette.websockets import WebSocket
|
from starlette.websockets import WebSocket
|
||||||
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
sequence_shapes = {
|
sequence_shapes = {
|
||||||
SHAPE_LIST,
|
SHAPE_LIST,
|
||||||
|
|
@ -112,18 +113,18 @@ def check_file_field(field: ModelField) -> None:
|
||||||
|
|
||||||
|
|
||||||
def get_param_sub_dependant(
|
def get_param_sub_dependant(
|
||||||
*, param: inspect.Parameter, path: str, security_scopes: Optional[List[str]] = None
|
*,
|
||||||
|
param_name: str,
|
||||||
|
depends: params.Depends,
|
||||||
|
path: str,
|
||||||
|
security_scopes: Optional[List[str]] = None,
|
||||||
) -> Dependant:
|
) -> Dependant:
|
||||||
depends: params.Depends = param.default
|
assert depends.dependency
|
||||||
if depends.dependency:
|
|
||||||
dependency = depends.dependency
|
|
||||||
else:
|
|
||||||
dependency = param.annotation
|
|
||||||
return get_sub_dependant(
|
return get_sub_dependant(
|
||||||
depends=depends,
|
depends=depends,
|
||||||
dependency=dependency,
|
dependency=depends.dependency,
|
||||||
path=path,
|
path=path,
|
||||||
name=param.name,
|
name=param_name,
|
||||||
security_scopes=security_scopes,
|
security_scopes=security_scopes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -298,122 +299,199 @@ def get_dependant(
|
||||||
use_cache=use_cache,
|
use_cache=use_cache,
|
||||||
)
|
)
|
||||||
for param_name, param in signature_params.items():
|
for param_name, param in signature_params.items():
|
||||||
if isinstance(param.default, params.Depends):
|
is_path_param = param_name in path_param_names
|
||||||
|
type_annotation, depends, param_field = analyze_param(
|
||||||
|
param_name=param_name,
|
||||||
|
annotation=param.annotation,
|
||||||
|
value=param.default,
|
||||||
|
is_path_param=is_path_param,
|
||||||
|
)
|
||||||
|
if depends is not None:
|
||||||
sub_dependant = get_param_sub_dependant(
|
sub_dependant = get_param_sub_dependant(
|
||||||
param=param, path=path, security_scopes=security_scopes
|
param_name=param_name,
|
||||||
|
depends=depends,
|
||||||
|
path=path,
|
||||||
|
security_scopes=security_scopes,
|
||||||
)
|
)
|
||||||
dependant.dependencies.append(sub_dependant)
|
dependant.dependencies.append(sub_dependant)
|
||||||
continue
|
continue
|
||||||
if add_non_field_param_to_dependency(param=param, dependant=dependant):
|
if add_non_field_param_to_dependency(
|
||||||
continue
|
|
||||||
param_field = get_param_field(
|
|
||||||
param=param, default_field_info=params.Query, param_name=param_name
|
|
||||||
)
|
|
||||||
if param_name in path_param_names:
|
|
||||||
assert is_scalar_field(
|
|
||||||
field=param_field
|
|
||||||
), "Path params must be of one of the supported types"
|
|
||||||
ignore_default = not isinstance(param.default, params.Path)
|
|
||||||
param_field = get_param_field(
|
|
||||||
param=param,
|
|
||||||
param_name=param_name,
|
param_name=param_name,
|
||||||
default_field_info=params.Path,
|
type_annotation=type_annotation,
|
||||||
force_type=params.ParamTypes.path,
|
dependant=dependant,
|
||||||
ignore_default=ignore_default,
|
):
|
||||||
)
|
assert (
|
||||||
add_param_to_fields(field=param_field, dependant=dependant)
|
param_field is None
|
||||||
elif is_scalar_field(field=param_field):
|
), f"Cannot specify multiple FastAPI annotations for {param_name!r}"
|
||||||
add_param_to_fields(field=param_field, dependant=dependant)
|
continue
|
||||||
elif isinstance(
|
assert param_field is not None
|
||||||
param.default, (params.Query, params.Header)
|
if is_body_param(param_field=param_field, is_path_param=is_path_param):
|
||||||
) and is_scalar_sequence_field(param_field):
|
|
||||||
add_param_to_fields(field=param_field, dependant=dependant)
|
|
||||||
else:
|
|
||||||
field_info = param_field.field_info
|
|
||||||
assert isinstance(
|
|
||||||
field_info, params.Body
|
|
||||||
), f"Param: {param_field.name} can only be a request body, using Body()"
|
|
||||||
dependant.body_params.append(param_field)
|
dependant.body_params.append(param_field)
|
||||||
|
else:
|
||||||
|
add_param_to_fields(field=param_field, dependant=dependant)
|
||||||
return dependant
|
return dependant
|
||||||
|
|
||||||
|
|
||||||
def add_non_field_param_to_dependency(
|
def add_non_field_param_to_dependency(
|
||||||
*, param: inspect.Parameter, dependant: Dependant
|
*, param_name: str, type_annotation: Any, dependant: Dependant
|
||||||
) -> Optional[bool]:
|
) -> Optional[bool]:
|
||||||
if lenient_issubclass(param.annotation, Request):
|
if lenient_issubclass(type_annotation, Request):
|
||||||
dependant.request_param_name = param.name
|
dependant.request_param_name = param_name
|
||||||
return True
|
return True
|
||||||
elif lenient_issubclass(param.annotation, WebSocket):
|
elif lenient_issubclass(type_annotation, WebSocket):
|
||||||
dependant.websocket_param_name = param.name
|
dependant.websocket_param_name = param_name
|
||||||
return True
|
return True
|
||||||
elif lenient_issubclass(param.annotation, HTTPConnection):
|
elif lenient_issubclass(type_annotation, HTTPConnection):
|
||||||
dependant.http_connection_param_name = param.name
|
dependant.http_connection_param_name = param_name
|
||||||
return True
|
return True
|
||||||
elif lenient_issubclass(param.annotation, Response):
|
elif lenient_issubclass(type_annotation, Response):
|
||||||
dependant.response_param_name = param.name
|
dependant.response_param_name = param_name
|
||||||
return True
|
return True
|
||||||
elif lenient_issubclass(param.annotation, BackgroundTasks):
|
elif lenient_issubclass(type_annotation, BackgroundTasks):
|
||||||
dependant.background_tasks_param_name = param.name
|
dependant.background_tasks_param_name = param_name
|
||||||
return True
|
return True
|
||||||
elif lenient_issubclass(param.annotation, SecurityScopes):
|
elif lenient_issubclass(type_annotation, SecurityScopes):
|
||||||
dependant.security_scopes_param_name = param.name
|
dependant.security_scopes_param_name = param_name
|
||||||
return True
|
return True
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_param_field(
|
def analyze_param(
|
||||||
*,
|
*,
|
||||||
param: inspect.Parameter,
|
|
||||||
param_name: str,
|
param_name: str,
|
||||||
default_field_info: Type[params.Param] = params.Param,
|
annotation: Any,
|
||||||
force_type: Optional[params.ParamTypes] = None,
|
value: Any,
|
||||||
ignore_default: bool = False,
|
is_path_param: bool,
|
||||||
) -> ModelField:
|
) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]:
|
||||||
default_value: Any = Undefined
|
field_info = None
|
||||||
had_schema = False
|
used_default_field_info = False
|
||||||
if not param.default == param.empty and ignore_default is False:
|
depends = None
|
||||||
default_value = param.default
|
type_annotation: Any = Any
|
||||||
if isinstance(default_value, FieldInfo):
|
|
||||||
had_schema = True
|
|
||||||
field_info = default_value
|
|
||||||
default_value = field_info.default
|
|
||||||
if (
|
if (
|
||||||
|
annotation is not inspect.Signature.empty
|
||||||
|
and get_origin(annotation) is Annotated # type: ignore[comparison-overlap]
|
||||||
|
):
|
||||||
|
annotated_args = get_args(annotation)
|
||||||
|
type_annotation = annotated_args[0]
|
||||||
|
fastapi_annotations = [
|
||||||
|
arg
|
||||||
|
for arg in annotated_args[1:]
|
||||||
|
if isinstance(arg, (FieldInfo, params.Depends))
|
||||||
|
]
|
||||||
|
assert (
|
||||||
|
len(fastapi_annotations) <= 1
|
||||||
|
), f"Cannot specify multiple `Annotated` FastAPI arguments for {param_name!r}"
|
||||||
|
fastapi_annotation = next(iter(fastapi_annotations), None)
|
||||||
|
if isinstance(fastapi_annotation, FieldInfo):
|
||||||
|
field_info = fastapi_annotation
|
||||||
|
assert field_info.default is Undefined or field_info.default is Required, (
|
||||||
|
f"`{field_info.__class__.__name__}` default value cannot be set in"
|
||||||
|
f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
|
||||||
|
)
|
||||||
|
if value is not inspect.Signature.empty:
|
||||||
|
assert not is_path_param, "Path parameters cannot have default values"
|
||||||
|
field_info.default = value
|
||||||
|
else:
|
||||||
|
field_info.default = Required
|
||||||
|
elif isinstance(fastapi_annotation, params.Depends):
|
||||||
|
depends = fastapi_annotation
|
||||||
|
elif annotation is not inspect.Signature.empty:
|
||||||
|
type_annotation = annotation
|
||||||
|
|
||||||
|
if isinstance(value, params.Depends):
|
||||||
|
assert depends is None, (
|
||||||
|
"Cannot specify `Depends` in `Annotated` and default value"
|
||||||
|
f" together for {param_name!r}"
|
||||||
|
)
|
||||||
|
assert field_info is None, (
|
||||||
|
"Cannot specify a FastAPI annotation in `Annotated` and `Depends` as a"
|
||||||
|
f" default value together for {param_name!r}"
|
||||||
|
)
|
||||||
|
depends = value
|
||||||
|
elif isinstance(value, FieldInfo):
|
||||||
|
assert field_info is None, (
|
||||||
|
"Cannot specify FastAPI annotations in `Annotated` and default value"
|
||||||
|
f" together for {param_name!r}"
|
||||||
|
)
|
||||||
|
field_info = value
|
||||||
|
|
||||||
|
if depends is not None and depends.dependency is None:
|
||||||
|
depends.dependency = type_annotation
|
||||||
|
|
||||||
|
if lenient_issubclass(
|
||||||
|
type_annotation,
|
||||||
|
(Request, WebSocket, HTTPConnection, Response, BackgroundTasks, SecurityScopes),
|
||||||
|
):
|
||||||
|
assert depends is None, f"Cannot specify `Depends` for type {type_annotation!r}"
|
||||||
|
assert (
|
||||||
|
field_info is None
|
||||||
|
), f"Cannot specify FastAPI annotation for type {type_annotation!r}"
|
||||||
|
elif field_info is None and depends is None:
|
||||||
|
default_value = value if value is not inspect.Signature.empty else Required
|
||||||
|
if is_path_param:
|
||||||
|
# We might check here that `default_value is Required`, but the fact is that the same
|
||||||
|
# parameter might sometimes be a path parameter and sometimes not. See
|
||||||
|
# `tests/test_infer_param_optionality.py` for an example.
|
||||||
|
field_info = params.Path()
|
||||||
|
else:
|
||||||
|
field_info = params.Query(default=default_value)
|
||||||
|
used_default_field_info = True
|
||||||
|
|
||||||
|
field = None
|
||||||
|
if field_info is not None:
|
||||||
|
if is_path_param:
|
||||||
|
assert isinstance(field_info, params.Path), (
|
||||||
|
f"Cannot use `{field_info.__class__.__name__}` for path param"
|
||||||
|
f" {param_name!r}"
|
||||||
|
)
|
||||||
|
elif (
|
||||||
isinstance(field_info, params.Param)
|
isinstance(field_info, params.Param)
|
||||||
and getattr(field_info, "in_", None) is None
|
and getattr(field_info, "in_", None) is None
|
||||||
):
|
):
|
||||||
field_info.in_ = default_field_info.in_
|
field_info.in_ = params.ParamTypes.query
|
||||||
if force_type:
|
annotation = get_annotation_from_field_info(
|
||||||
field_info.in_ = force_type # type: ignore
|
annotation if annotation is not inspect.Signature.empty else Any,
|
||||||
else:
|
field_info,
|
||||||
field_info = default_field_info(default=default_value)
|
param_name,
|
||||||
required = True
|
)
|
||||||
if default_value is Required or ignore_default:
|
|
||||||
required = True
|
|
||||||
default_value = None
|
|
||||||
elif default_value is not Undefined:
|
|
||||||
required = False
|
|
||||||
annotation: Any = Any
|
|
||||||
if not param.annotation == param.empty:
|
|
||||||
annotation = param.annotation
|
|
||||||
annotation = get_annotation_from_field_info(annotation, field_info, param_name)
|
|
||||||
if not field_info.alias and getattr(field_info, "convert_underscores", None):
|
if not field_info.alias and getattr(field_info, "convert_underscores", None):
|
||||||
alias = param.name.replace("_", "-")
|
alias = param_name.replace("_", "-")
|
||||||
else:
|
else:
|
||||||
alias = field_info.alias or param.name
|
alias = field_info.alias or param_name
|
||||||
field = create_response_field(
|
field = create_response_field(
|
||||||
name=param.name,
|
name=param_name,
|
||||||
type_=annotation,
|
type_=annotation,
|
||||||
default=default_value,
|
default=field_info.default,
|
||||||
alias=alias,
|
alias=alias,
|
||||||
required=required,
|
required=field_info.default in (Required, Undefined),
|
||||||
field_info=field_info,
|
field_info=field_info,
|
||||||
)
|
)
|
||||||
if not had_schema and not is_scalar_field(field=field):
|
if used_default_field_info:
|
||||||
field.field_info = params.Body(field_info.default)
|
if lenient_issubclass(field.type_, UploadFile):
|
||||||
if not had_schema and lenient_issubclass(field.type_, UploadFile):
|
|
||||||
field.field_info = params.File(field_info.default)
|
field.field_info = params.File(field_info.default)
|
||||||
|
elif not is_scalar_field(field=field):
|
||||||
|
field.field_info = params.Body(field_info.default)
|
||||||
|
|
||||||
return field
|
return type_annotation, depends, field
|
||||||
|
|
||||||
|
|
||||||
|
def is_body_param(*, param_field: ModelField, is_path_param: bool) -> bool:
|
||||||
|
if is_path_param:
|
||||||
|
assert is_scalar_field(
|
||||||
|
field=param_field
|
||||||
|
), "Path params must be of one of the supported types"
|
||||||
|
return False
|
||||||
|
elif is_scalar_field(field=param_field):
|
||||||
|
return False
|
||||||
|
elif isinstance(
|
||||||
|
param_field.field_info, (params.Query, params.Header)
|
||||||
|
) and is_scalar_sequence_field(param_field):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
assert isinstance(
|
||||||
|
param_field.field_info, params.Body
|
||||||
|
), f"Param: {param_field.name} can only be a request body, using Body()"
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None:
|
def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None:
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from pydantic.fields import Undefined
|
||||||
|
|
||||||
|
|
||||||
def Path( # noqa: N802
|
def Path( # noqa: N802
|
||||||
default: Any = Undefined,
|
default: Any = ...,
|
||||||
*,
|
*,
|
||||||
alias: Optional[str] = None,
|
alias: Optional[str] = None,
|
||||||
title: Optional[str] = None,
|
title: Optional[str] = None,
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ class Path(Param):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
default: Any = Undefined,
|
default: Any = ...,
|
||||||
*,
|
*,
|
||||||
alias: Optional[str] = None,
|
alias: Optional[str] = None,
|
||||||
title: Optional[str] = None,
|
title: Optional[str] = None,
|
||||||
|
|
@ -80,9 +80,10 @@ class Path(Param):
|
||||||
include_in_schema: bool = True,
|
include_in_schema: bool = True,
|
||||||
**extra: Any,
|
**extra: Any,
|
||||||
):
|
):
|
||||||
|
assert default is ..., "Path parameters cannot have a default value"
|
||||||
self.in_ = self.in_
|
self.in_ = self.in_
|
||||||
super().__init__(
|
super().__init__(
|
||||||
default=...,
|
default=default,
|
||||||
alias=alias,
|
alias=alias,
|
||||||
title=title,
|
title=title,
|
||||||
description=description,
|
description=description,
|
||||||
|
|
@ -279,7 +280,7 @@ class Body(FieldInfo):
|
||||||
class Form(Body):
|
class Form(Body):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
default: Any,
|
default: Any = Undefined,
|
||||||
*,
|
*,
|
||||||
media_type: str = "application/x-www-form-urlencoded",
|
media_type: str = "application/x-www-form-urlencoded",
|
||||||
alias: Optional[str] = None,
|
alias: Optional[str] = None,
|
||||||
|
|
@ -319,7 +320,7 @@ class Form(Body):
|
||||||
class File(Form):
|
class File(Form):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
default: Any,
|
default: Any = Undefined,
|
||||||
*,
|
*,
|
||||||
media_type: str = "multipart/form-data",
|
media_type: str = "multipart/form-data",
|
||||||
alias: Optional[str] = None,
|
alias: Optional[str] = None,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import functools
|
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
from dataclasses import is_dataclass
|
from dataclasses import is_dataclass
|
||||||
|
|
@ -73,8 +72,8 @@ def create_response_field(
|
||||||
class_validators = class_validators or {}
|
class_validators = class_validators or {}
|
||||||
field_info = field_info or FieldInfo()
|
field_info = field_info or FieldInfo()
|
||||||
|
|
||||||
response_field = functools.partial(
|
try:
|
||||||
ModelField,
|
return ModelField(
|
||||||
name=name,
|
name=name,
|
||||||
type_=type_,
|
type_=type_,
|
||||||
class_validators=class_validators,
|
class_validators=class_validators,
|
||||||
|
|
@ -82,10 +81,8 @@ def create_response_field(
|
||||||
required=required,
|
required=required,
|
||||||
model_config=model_config,
|
model_config=model_config,
|
||||||
alias=alias,
|
alias=alias,
|
||||||
|
field_info=field_info,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
|
||||||
return response_field(field_info=field_info)
|
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
raise fastapi.exceptions.FastAPIError(
|
raise fastapi.exceptions.FastAPIError(
|
||||||
"Invalid args for response field! Hint: "
|
"Invalid args for response field! Hint: "
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,7 @@ def get_bool_id(item_id: bool):
|
||||||
|
|
||||||
|
|
||||||
@app.get("/path/param/{item_id}")
|
@app.get("/path/param/{item_id}")
|
||||||
def get_path_param_id(item_id: str = Path()):
|
def get_path_param_id(item_id: Optional[str] = Path()):
|
||||||
return item_id
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/path/param-required/{item_id}")
|
|
||||||
def get_path_param_required_id(item_id: str = Path()):
|
|
||||||
return item_id
|
return item_id
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import pytest
|
||||||
|
from fastapi import Depends, FastAPI, Path
|
||||||
|
from fastapi.param_functions import Query
|
||||||
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_annotated_defaults():
|
||||||
|
with pytest.raises(
|
||||||
|
AssertionError, match="Path parameters cannot have a default value"
|
||||||
|
):
|
||||||
|
|
||||||
|
@app.get("/items/{item_id}/")
|
||||||
|
async def get_item(item_id: Annotated[int, Path(default=1)]):
|
||||||
|
pass # pragma: nocover
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
AssertionError,
|
||||||
|
match=(
|
||||||
|
"`Query` default value cannot be set in `Annotated` for 'item_id'. Set the"
|
||||||
|
" default value with `=` instead."
|
||||||
|
),
|
||||||
|
):
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def get(item_id: Annotated[int, Query(default=1)]):
|
||||||
|
pass # pragma: nocover
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_multiple_annotations():
|
||||||
|
async def dep():
|
||||||
|
pass # pragma: nocover
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
AssertionError,
|
||||||
|
match="Cannot specify multiple `Annotated` FastAPI arguments for 'foo'",
|
||||||
|
):
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def get(foo: Annotated[int, Query(min_length=1), Query()]):
|
||||||
|
pass # pragma: nocover
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
AssertionError,
|
||||||
|
match=(
|
||||||
|
"Cannot specify `Depends` in `Annotated` and default value"
|
||||||
|
" together for 'foo'"
|
||||||
|
),
|
||||||
|
):
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def get2(foo: Annotated[int, Depends(dep)] = Depends(dep)):
|
||||||
|
pass # pragma: nocover
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
AssertionError,
|
||||||
|
match=(
|
||||||
|
"Cannot specify a FastAPI annotation in `Annotated` and `Depends` as a"
|
||||||
|
" default value together for 'foo'"
|
||||||
|
),
|
||||||
|
):
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def get3(foo: Annotated[int, Query(min_length=1)] = Depends(dep)):
|
||||||
|
pass # pragma: nocover
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
import pytest
|
||||||
|
from fastapi import FastAPI, Query
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/default")
|
||||||
|
async def default(foo: Annotated[str, Query()] = "foo"):
|
||||||
|
return {"foo": foo}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/required")
|
||||||
|
async def required(foo: Annotated[str, Query(min_length=1)]):
|
||||||
|
return {"foo": foo}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/multiple")
|
||||||
|
async def multiple(foo: Annotated[str, object(), Query(min_length=1)]):
|
||||||
|
return {"foo": foo}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/unrelated")
|
||||||
|
async def unrelated(foo: Annotated[str, object()]):
|
||||||
|
return {"foo": foo}
|
||||||
|
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
openapi_schema = {
|
||||||
|
"openapi": "3.0.2",
|
||||||
|
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||||
|
"paths": {
|
||||||
|
"/default": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Default",
|
||||||
|
"operationId": "default_default_get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"schema": {"title": "Foo", "type": "string", "default": "foo"},
|
||||||
|
"name": "foo",
|
||||||
|
"in": "query",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/required": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Required",
|
||||||
|
"operationId": "required_required_get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": True,
|
||||||
|
"schema": {"title": "Foo", "minLength": 1, "type": "string"},
|
||||||
|
"name": "foo",
|
||||||
|
"in": "query",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/multiple": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Multiple",
|
||||||
|
"operationId": "multiple_multiple_get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": True,
|
||||||
|
"schema": {"title": "Foo", "minLength": 1, "type": "string"},
|
||||||
|
"name": "foo",
|
||||||
|
"in": "query",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/unrelated": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Unrelated",
|
||||||
|
"operationId": "unrelated_unrelated_get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": True,
|
||||||
|
"schema": {"title": "Foo", "type": "string"},
|
||||||
|
"name": "foo",
|
||||||
|
"in": "query",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"HTTPValidationError": {
|
||||||
|
"title": "HTTPValidationError",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"detail": {
|
||||||
|
"title": "Detail",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ValidationError": {
|
||||||
|
"title": "ValidationError",
|
||||||
|
"required": ["loc", "msg", "type"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"loc": {
|
||||||
|
"title": "Location",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
|
||||||
|
},
|
||||||
|
"msg": {"title": "Message", "type": "string"},
|
||||||
|
"type": {"title": "Error Type", "type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
foo_is_missing = {
|
||||||
|
"detail": [
|
||||||
|
{
|
||||||
|
"loc": ["query", "foo"],
|
||||||
|
"msg": "field required",
|
||||||
|
"type": "value_error.missing",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
foo_is_short = {
|
||||||
|
"detail": [
|
||||||
|
{
|
||||||
|
"ctx": {"limit_value": 1},
|
||||||
|
"loc": ["query", "foo"],
|
||||||
|
"msg": "ensure this value has at least 1 characters",
|
||||||
|
"type": "value_error.any_str.min_length",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path,expected_status,expected_response",
|
||||||
|
[
|
||||||
|
("/default", 200, {"foo": "foo"}),
|
||||||
|
("/default?foo=bar", 200, {"foo": "bar"}),
|
||||||
|
("/required?foo=bar", 200, {"foo": "bar"}),
|
||||||
|
("/required", 422, foo_is_missing),
|
||||||
|
("/required?foo=", 422, foo_is_short),
|
||||||
|
("/multiple?foo=bar", 200, {"foo": "bar"}),
|
||||||
|
("/multiple", 422, foo_is_missing),
|
||||||
|
("/multiple?foo=", 422, foo_is_short),
|
||||||
|
("/unrelated?foo=bar", 200, {"foo": "bar"}),
|
||||||
|
("/unrelated", 422, foo_is_missing),
|
||||||
|
("/openapi.json", 200, openapi_schema),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get(path, expected_status, expected_response):
|
||||||
|
response = client.get(path)
|
||||||
|
assert response.status_code == expected_status
|
||||||
|
assert response.json() == expected_response
|
||||||
|
|
@ -225,36 +225,6 @@ openapi_schema = {
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/path/param-required/{item_id}": {
|
|
||||||
"get": {
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {"application/json": {"schema": {}}},
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"summary": "Get Path Param Required Id",
|
|
||||||
"operationId": "get_path_param_required_id_path_param_required__item_id__get",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"required": True,
|
|
||||||
"schema": {"title": "Item Id", "type": "string"},
|
|
||||||
"name": "item_id",
|
|
||||||
"in": "path",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/path/param-minlength/{item_id}": {
|
"/path/param-minlength/{item_id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"responses": {
|
"responses": {
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,9 @@ def test_param_repr(params):
|
||||||
assert repr(Param(params)) == "Param(" + str(params) + ")"
|
assert repr(Param(params)) == "Param(" + str(params) + ")"
|
||||||
|
|
||||||
|
|
||||||
def test_path_repr(params):
|
def test_path_repr():
|
||||||
assert repr(Path(params)) == "Path(Ellipsis)"
|
assert repr(Path()) == "Path(Ellipsis)"
|
||||||
|
assert repr(Path(...)) == "Path(Ellipsis)"
|
||||||
|
|
||||||
|
|
||||||
def test_query_repr(params):
|
def test_query_repr(params):
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,6 @@ response_less_than_equal_3 = {
|
||||||
("/path/bool/False", 200, False),
|
("/path/bool/False", 200, False),
|
||||||
("/path/bool/false", 200, False),
|
("/path/bool/false", 200, False),
|
||||||
("/path/param/foo", 200, "foo"),
|
("/path/param/foo", 200, "foo"),
|
||||||
("/path/param-required/foo", 200, "foo"),
|
|
||||||
("/path/param-minlength/foo", 200, "foo"),
|
("/path/param-minlength/foo", 200, "foo"),
|
||||||
("/path/param-minlength/fo", 422, response_at_least_3),
|
("/path/param-minlength/fo", 422, response_at_least_3),
|
||||||
("/path/param-maxlength/foo", 200, "foo"),
|
("/path/param-maxlength/foo", 200, "foo"),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from docs_src.annotated.tutorial001 import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
openapi_schema = {
|
||||||
|
"openapi": "3.0.2",
|
||||||
|
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||||
|
"paths": {
|
||||||
|
"/items/": {
|
||||||
|
"get": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"summary": "Read Items",
|
||||||
|
"operationId": "read_items_items__get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"schema": {"title": "Q", "type": "string"},
|
||||||
|
"name": "q",
|
||||||
|
"in": "query",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"schema": {"title": "Skip", "type": "integer", "default": 0},
|
||||||
|
"name": "skip",
|
||||||
|
"in": "query",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"schema": {"title": "Limit", "type": "integer", "default": 100},
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"ValidationError": {
|
||||||
|
"title": "ValidationError",
|
||||||
|
"required": ["loc", "msg", "type"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"loc": {
|
||||||
|
"title": "Location",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
|
||||||
|
},
|
||||||
|
"msg": {"title": "Message", "type": "string"},
|
||||||
|
"type": {"title": "Error Type", "type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"HTTPValidationError": {
|
||||||
|
"title": "HTTPValidationError",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"detail": {
|
||||||
|
"title": "Detail",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path,expected_status,expected_response",
|
||||||
|
[
|
||||||
|
("/items", 200, {"q": None, "skip": 0, "limit": 100}),
|
||||||
|
("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}),
|
||||||
|
("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}),
|
||||||
|
("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}),
|
||||||
|
("/openapi.json", 200, openapi_schema),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get(path, expected_status, expected_response):
|
||||||
|
response = client.get(path)
|
||||||
|
assert response.status_code == expected_status
|
||||||
|
assert response.json() == expected_response
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from ...utils import needs_py39
|
||||||
|
|
||||||
|
openapi_schema = {
|
||||||
|
"openapi": "3.0.2",
|
||||||
|
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||||
|
"paths": {
|
||||||
|
"/items/": {
|
||||||
|
"get": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"summary": "Read Items",
|
||||||
|
"operationId": "read_items_items__get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"schema": {"title": "Q", "type": "string"},
|
||||||
|
"name": "q",
|
||||||
|
"in": "query",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"schema": {"title": "Skip", "type": "integer", "default": 0},
|
||||||
|
"name": "skip",
|
||||||
|
"in": "query",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"schema": {"title": "Limit", "type": "integer", "default": 100},
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"ValidationError": {
|
||||||
|
"title": "ValidationError",
|
||||||
|
"required": ["loc", "msg", "type"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"loc": {
|
||||||
|
"title": "Location",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
|
||||||
|
},
|
||||||
|
"msg": {"title": "Message", "type": "string"},
|
||||||
|
"type": {"title": "Error Type", "type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"HTTPValidationError": {
|
||||||
|
"title": "HTTPValidationError",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"detail": {
|
||||||
|
"title": "Detail",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="client")
|
||||||
|
def get_client():
|
||||||
|
from docs_src.annotated.tutorial001_py39 import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
@needs_py39
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path,expected_status,expected_response",
|
||||||
|
[
|
||||||
|
("/items", 200, {"q": None, "skip": 0, "limit": 100}),
|
||||||
|
("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}),
|
||||||
|
("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}),
|
||||||
|
("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}),
|
||||||
|
("/openapi.json", 200, openapi_schema),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get(path, expected_status, expected_response, client):
|
||||||
|
response = client.get(path)
|
||||||
|
assert response.status_code == expected_status
|
||||||
|
assert response.json() == expected_response
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from docs_src.annotated.tutorial002 import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
openapi_schema = {
|
||||||
|
"openapi": "3.0.2",
|
||||||
|
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||||
|
"paths": {
|
||||||
|
"/items/": {
|
||||||
|
"get": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"summary": "Read Items",
|
||||||
|
"operationId": "read_items_items__get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"schema": {"title": "Q", "type": "string"},
|
||||||
|
"name": "q",
|
||||||
|
"in": "query",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"schema": {"title": "Skip", "type": "integer", "default": 0},
|
||||||
|
"name": "skip",
|
||||||
|
"in": "query",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"schema": {"title": "Limit", "type": "integer", "default": 100},
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"ValidationError": {
|
||||||
|
"title": "ValidationError",
|
||||||
|
"required": ["loc", "msg", "type"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"loc": {
|
||||||
|
"title": "Location",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
|
||||||
|
},
|
||||||
|
"msg": {"title": "Message", "type": "string"},
|
||||||
|
"type": {"title": "Error Type", "type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"HTTPValidationError": {
|
||||||
|
"title": "HTTPValidationError",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"detail": {
|
||||||
|
"title": "Detail",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path,expected_status,expected_response",
|
||||||
|
[
|
||||||
|
("/items", 200, {"q": None, "skip": 0, "limit": 100}),
|
||||||
|
("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}),
|
||||||
|
("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}),
|
||||||
|
("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}),
|
||||||
|
("/openapi.json", 200, openapi_schema),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get(path, expected_status, expected_response):
|
||||||
|
response = client.get(path)
|
||||||
|
assert response.status_code == expected_status
|
||||||
|
assert response.json() == expected_response
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from ...utils import needs_py39
|
||||||
|
|
||||||
|
openapi_schema = {
|
||||||
|
"openapi": "3.0.2",
|
||||||
|
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||||
|
"paths": {
|
||||||
|
"/items/": {
|
||||||
|
"get": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"summary": "Read Items",
|
||||||
|
"operationId": "read_items_items__get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"schema": {"title": "Q", "type": "string"},
|
||||||
|
"name": "q",
|
||||||
|
"in": "query",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"schema": {"title": "Skip", "type": "integer", "default": 0},
|
||||||
|
"name": "skip",
|
||||||
|
"in": "query",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"schema": {"title": "Limit", "type": "integer", "default": 100},
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"ValidationError": {
|
||||||
|
"title": "ValidationError",
|
||||||
|
"required": ["loc", "msg", "type"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"loc": {
|
||||||
|
"title": "Location",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
|
||||||
|
},
|
||||||
|
"msg": {"title": "Message", "type": "string"},
|
||||||
|
"type": {"title": "Error Type", "type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"HTTPValidationError": {
|
||||||
|
"title": "HTTPValidationError",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"detail": {
|
||||||
|
"title": "Detail",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="client")
|
||||||
|
def get_client():
|
||||||
|
from docs_src.annotated.tutorial002_py39 import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
@needs_py39
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path,expected_status,expected_response",
|
||||||
|
[
|
||||||
|
("/items", 200, {"q": None, "skip": 0, "limit": 100}),
|
||||||
|
("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}),
|
||||||
|
("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}),
|
||||||
|
("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}),
|
||||||
|
("/openapi.json", 200, openapi_schema),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get(path, expected_status, expected_response, client):
|
||||||
|
response = client.get(path)
|
||||||
|
assert response.status_code == expected_status
|
||||||
|
assert response.json() == expected_response
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from docs_src.annotated.tutorial003 import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
openapi_schema = {
|
||||||
|
"openapi": "3.0.2",
|
||||||
|
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||||
|
"paths": {
|
||||||
|
"/items/{item_id}": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Read Items",
|
||||||
|
"operationId": "read_items_items__item_id__get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": True,
|
||||||
|
"schema": {
|
||||||
|
"title": "Item Id",
|
||||||
|
"exclusiveMinimum": 0.0,
|
||||||
|
"type": "integer",
|
||||||
|
},
|
||||||
|
"name": "item_id",
|
||||||
|
"in": "path",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/users": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Read Users",
|
||||||
|
"operationId": "read_users_users_get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"schema": {
|
||||||
|
"title": "User Id",
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string",
|
||||||
|
"default": "me",
|
||||||
|
},
|
||||||
|
"name": "user_id",
|
||||||
|
"in": "query",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"HTTPValidationError": {
|
||||||
|
"title": "HTTPValidationError",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"detail": {
|
||||||
|
"title": "Detail",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ValidationError": {
|
||||||
|
"title": "ValidationError",
|
||||||
|
"required": ["loc", "msg", "type"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"loc": {
|
||||||
|
"title": "Location",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
|
||||||
|
},
|
||||||
|
"msg": {"title": "Message", "type": "string"},
|
||||||
|
"type": {"title": "Error Type", "type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
item_id_negative = {
|
||||||
|
"detail": [
|
||||||
|
{
|
||||||
|
"ctx": {"limit_value": 0},
|
||||||
|
"loc": ["path", "item_id"],
|
||||||
|
"msg": "ensure this value is greater than 0",
|
||||||
|
"type": "value_error.number.not_gt",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path,expected_status,expected_response",
|
||||||
|
[
|
||||||
|
("/items/1", 200, {"item_id": 1}),
|
||||||
|
("/items/-1", 422, item_id_negative),
|
||||||
|
("/users", 200, {"user_id": "me"}),
|
||||||
|
("/users?user_id=foo", 200, {"user_id": "foo"}),
|
||||||
|
("/openapi.json", 200, openapi_schema),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get(path, expected_status, expected_response):
|
||||||
|
response = client.get(path)
|
||||||
|
assert response.status_code == expected_status, response.text
|
||||||
|
assert response.json() == expected_response
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from ...utils import needs_py39
|
||||||
|
|
||||||
|
openapi_schema = {
|
||||||
|
"openapi": "3.0.2",
|
||||||
|
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||||
|
"paths": {
|
||||||
|
"/items/{item_id}": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Read Items",
|
||||||
|
"operationId": "read_items_items__item_id__get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": True,
|
||||||
|
"schema": {
|
||||||
|
"title": "Item Id",
|
||||||
|
"exclusiveMinimum": 0.0,
|
||||||
|
"type": "integer",
|
||||||
|
},
|
||||||
|
"name": "item_id",
|
||||||
|
"in": "path",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/users": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Read Users",
|
||||||
|
"operationId": "read_users_users_get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"schema": {
|
||||||
|
"title": "User Id",
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string",
|
||||||
|
"default": "me",
|
||||||
|
},
|
||||||
|
"name": "user_id",
|
||||||
|
"in": "query",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"HTTPValidationError": {
|
||||||
|
"title": "HTTPValidationError",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"detail": {
|
||||||
|
"title": "Detail",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ValidationError": {
|
||||||
|
"title": "ValidationError",
|
||||||
|
"required": ["loc", "msg", "type"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"loc": {
|
||||||
|
"title": "Location",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
|
||||||
|
},
|
||||||
|
"msg": {"title": "Message", "type": "string"},
|
||||||
|
"type": {"title": "Error Type", "type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
item_id_negative = {
|
||||||
|
"detail": [
|
||||||
|
{
|
||||||
|
"ctx": {"limit_value": 0},
|
||||||
|
"loc": ["path", "item_id"],
|
||||||
|
"msg": "ensure this value is greater than 0",
|
||||||
|
"type": "value_error.number.not_gt",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="client")
|
||||||
|
def get_client():
|
||||||
|
from docs_src.annotated.tutorial003_py39 import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
@needs_py39
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path,expected_status,expected_response",
|
||||||
|
[
|
||||||
|
("/items/1", 200, {"item_id": 1}),
|
||||||
|
("/items/-1", 422, item_id_negative),
|
||||||
|
("/users", 200, {"user_id": "me"}),
|
||||||
|
("/users?user_id=foo", 200, {"user_id": "foo"}),
|
||||||
|
("/openapi.json", 200, openapi_schema),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get(path, expected_status, expected_response, client):
|
||||||
|
response = client.get(path)
|
||||||
|
assert response.status_code == expected_status, response.text
|
||||||
|
assert response.json() == expected_response
|
||||||
Loading…
Reference in New Issue