mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-24 06:39:31 -05:00
Compare commits
6 Commits
0.115.7
...
0.100.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dc975da9d | ||
|
|
e7b3d47af3 | ||
|
|
cfb00b2119 | ||
|
|
c58e2b2d1e | ||
|
|
5301cfff52 | ||
|
|
bd32fecaf6 |
13
.github/workflows/test.yml
vendored
13
.github/workflows/test.yml
vendored
@@ -25,10 +25,12 @@ jobs:
|
||||
id: cache
|
||||
with:
|
||||
path: ${{ env.pythonLocation }}
|
||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
|
||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
|
||||
- name: Install Dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: pip install -r requirements-tests.txt
|
||||
- name: Install Pydantic v2
|
||||
run: pip install --pre "pydantic>=2.0.0b2,<3.0.0"
|
||||
- name: Lint
|
||||
run: bash scripts/lint.sh
|
||||
|
||||
@@ -37,6 +39,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||
pydantic-version: ["pydantic-v1", "pydantic-v2"]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -51,10 +54,16 @@ jobs:
|
||||
id: cache
|
||||
with:
|
||||
path: ${{ env.pythonLocation }}
|
||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
|
||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
|
||||
- name: Install Dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: pip install -r requirements-tests.txt
|
||||
- name: Install Pydantic v1
|
||||
if: matrix.pydantic-version == 'pydantic-v1'
|
||||
run: pip install "pydantic>=1.10.0,<2.0.0"
|
||||
- name: Install Pydantic v2
|
||||
if: matrix.pydantic-version == 'pydantic-v2'
|
||||
run: pip install --pre "pydantic>=2.0.0b2,<3.0.0"
|
||||
- run: mkdir coverage
|
||||
- name: Test
|
||||
run: bash scripts/test.sh
|
||||
|
||||
@@ -12,11 +12,11 @@ class BaseItem(BaseModel):
|
||||
|
||||
|
||||
class CarItem(BaseItem):
|
||||
type = "car"
|
||||
type: str = "car"
|
||||
|
||||
|
||||
class PlaneItem(BaseItem):
|
||||
type = "plane"
|
||||
type: str = "plane"
|
||||
size: int
|
||||
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ class BaseItem(BaseModel):
|
||||
|
||||
|
||||
class CarItem(BaseItem):
|
||||
type = "car"
|
||||
type: str = "car"
|
||||
|
||||
|
||||
class PlaneItem(BaseItem):
|
||||
type = "plane"
|
||||
type: str = "plane"
|
||||
size: int
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.97.0"
|
||||
__version__ = "0.100.0-beta1"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
597
fastapi/_compat.py
Normal file
597
fastapi/_compat.py
Normal file
@@ -0,0 +1,597 @@
|
||||
from collections import deque
|
||||
from copy import copy
|
||||
from dataclasses import dataclass, is_dataclass
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Deque,
|
||||
Dict,
|
||||
FrozenSet,
|
||||
List,
|
||||
Mapping,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
from fastapi.exceptions import RequestErrorModel
|
||||
from fastapi.types import IncEx, ModelNameMap, UnionType
|
||||
from pydantic import BaseModel, create_model
|
||||
from pydantic.version import VERSION as PYDANTIC_VERSION
|
||||
from starlette.datastructures import UploadFile
|
||||
from typing_extensions import Annotated, Literal, get_args, get_origin
|
||||
|
||||
PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.")
|
||||
|
||||
|
||||
sequence_annotation_to_type = {
|
||||
Sequence: list,
|
||||
List: list,
|
||||
list: list,
|
||||
Tuple: tuple,
|
||||
tuple: tuple,
|
||||
Set: set,
|
||||
set: set,
|
||||
FrozenSet: frozenset,
|
||||
frozenset: frozenset,
|
||||
Deque: deque,
|
||||
deque: deque,
|
||||
}
|
||||
|
||||
sequence_types = tuple(sequence_annotation_to_type.keys())
|
||||
|
||||
if PYDANTIC_V2:
|
||||
from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
|
||||
from pydantic import TypeAdapter
|
||||
from pydantic import ValidationError as ValidationError
|
||||
from pydantic._internal._fields import Undefined as Undefined
|
||||
from pydantic._internal._fields import _UndefinedType
|
||||
from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined]
|
||||
GetJsonSchemaHandler as GetJsonSchemaHandler,
|
||||
)
|
||||
from pydantic._internal._typing_extra import eval_type_lenient
|
||||
from pydantic._internal._utils import lenient_issubclass as lenient_issubclass
|
||||
from pydantic.fields import FieldInfo
|
||||
from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema
|
||||
from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue
|
||||
from pydantic_core import CoreSchema as CoreSchema
|
||||
from pydantic_core import ErrorDetails
|
||||
from pydantic_core import MultiHostUrl as MultiHostUrl
|
||||
from pydantic_core import Url as Url
|
||||
from pydantic_core.core_schema import (
|
||||
general_plain_validator_function as general_plain_validator_function,
|
||||
)
|
||||
|
||||
Required = Undefined
|
||||
UndefinedType = _UndefinedType
|
||||
evaluate_forwardref = eval_type_lenient
|
||||
Validator = Any
|
||||
|
||||
class BaseConfig:
|
||||
pass
|
||||
|
||||
class ErrorWrapper(Exception):
|
||||
pass
|
||||
|
||||
@dataclass
|
||||
class ModelField:
|
||||
field_info: FieldInfo
|
||||
name: str
|
||||
|
||||
@property
|
||||
def alias(self) -> str:
|
||||
a = self.field_info.alias
|
||||
return a if a is not None else self.name
|
||||
|
||||
@property
|
||||
def required(self) -> bool:
|
||||
return self.field_info.is_required()
|
||||
|
||||
@property
|
||||
def default(self) -> Any:
|
||||
return self.get_default()
|
||||
|
||||
@property
|
||||
def type_(self) -> Any:
|
||||
return self.field_info.annotation
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self._type_adapter: TypeAdapter[Any] = TypeAdapter(
|
||||
Annotated[self.field_info.annotation, self.field_info]
|
||||
)
|
||||
|
||||
def get_default(self) -> Any:
|
||||
if self.field_info.is_required():
|
||||
return Undefined
|
||||
return self.field_info.get_default(call_default_factory=True)
|
||||
|
||||
def validate(
|
||||
self,
|
||||
value: Any,
|
||||
values: Dict[str, Any] = {}, # noqa: B006
|
||||
*,
|
||||
loc: Tuple[Union[int, str], ...] = (),
|
||||
) -> Tuple[Any, Union[List[Dict[str, Any]], None]]:
|
||||
try:
|
||||
return (
|
||||
self._type_adapter.validate_python(value, from_attributes=True),
|
||||
None,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
return None, _regenerate_error_with_loc(
|
||||
errors=exc.errors(), loc_prefix=loc
|
||||
)
|
||||
|
||||
def serialize(
|
||||
self,
|
||||
value: Any,
|
||||
*,
|
||||
mode: Literal["json", "python"] = "json",
|
||||
include: Union[IncEx, None] = None,
|
||||
exclude: Union[IncEx, None] = None,
|
||||
by_alias: bool = True,
|
||||
exclude_unset: bool = False,
|
||||
exclude_defaults: bool = False,
|
||||
exclude_none: bool = False,
|
||||
) -> Any:
|
||||
# What calls this code passes a value that already called
|
||||
# self._type_adapter.validate_python(value)
|
||||
return self._type_adapter.dump_python(
|
||||
value,
|
||||
mode=mode,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
# Each ModelField is unique for our purposes, to allow making a dict from
|
||||
# ModelField to its JSON Schema.
|
||||
return id(self)
|
||||
|
||||
def get_annotation_from_field_info(
|
||||
annotation: Any, field_info: FieldInfo, field_name: str
|
||||
) -> Any:
|
||||
return annotation
|
||||
|
||||
def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
|
||||
return errors # type: ignore[return-value]
|
||||
|
||||
def _model_rebuild(model: Type[BaseModel]) -> None:
|
||||
model.model_rebuild()
|
||||
|
||||
def _model_dump(
|
||||
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
|
||||
) -> Any:
|
||||
return model.model_dump(mode=mode, **kwargs)
|
||||
|
||||
def _get_model_config(model: BaseModel) -> Any:
|
||||
return model.model_config
|
||||
|
||||
def get_schema_from_model_field(
|
||||
*,
|
||||
field: ModelField,
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
) -> Dict[str, Any]:
|
||||
# This expects that GenerateJsonSchema was already used to generate the definitions
|
||||
json_schema = schema_generator.generate_inner(field._type_adapter.core_schema)
|
||||
if "$ref" not in json_schema:
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
# Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207
|
||||
json_schema[
|
||||
"title"
|
||||
] = field.field_info.title or field.alias.title().replace("_", " ")
|
||||
return json_schema
|
||||
|
||||
def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
|
||||
return {}
|
||||
|
||||
def get_definitions(
|
||||
*,
|
||||
fields: List[ModelField],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
inputs = [
|
||||
(field, "validation", field._type_adapter.core_schema) for field in fields
|
||||
]
|
||||
_, definitions = schema_generator.generate_definitions(inputs=inputs) # type: ignore[arg-type]
|
||||
return definitions # type: ignore[return-value]
|
||||
|
||||
def is_scalar_field(field: ModelField) -> bool:
|
||||
from fastapi import params
|
||||
|
||||
return field_annotation_is_scalar(
|
||||
field.field_info.annotation
|
||||
) and not isinstance(field.field_info, params.Body)
|
||||
|
||||
def is_sequence_field(field: ModelField) -> bool:
|
||||
return field_annotation_is_sequence(field.field_info.annotation)
|
||||
|
||||
def is_scalar_sequence_field(field: ModelField) -> bool:
|
||||
return field_annotation_is_scalar_sequence(field.field_info.annotation)
|
||||
|
||||
def is_bytes_field(field: ModelField) -> bool:
|
||||
return is_bytes_or_nonable_bytes_annotation(field.type_)
|
||||
|
||||
def is_bytes_sequence_field(field: ModelField) -> bool:
|
||||
return is_bytes_sequence_annotation(field.type_)
|
||||
|
||||
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
|
||||
return type(field_info).from_annotation(annotation)
|
||||
|
||||
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
|
||||
origin_type = (
|
||||
get_origin(field.field_info.annotation) or field.field_info.annotation
|
||||
)
|
||||
assert issubclass(origin_type, sequence_types) # type: ignore[arg-type]
|
||||
return sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return]
|
||||
|
||||
def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
|
||||
error = ValidationError.from_exception_data(
|
||||
"Field required", [{"type": "missing", "loc": loc, "input": {}}]
|
||||
).errors()[0]
|
||||
error["input"] = None
|
||||
return error # type: ignore[return-value]
|
||||
|
||||
def create_body_model(
|
||||
*, fields: Sequence[ModelField], model_name: str
|
||||
) -> Type[BaseModel]:
|
||||
field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields}
|
||||
BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload]
|
||||
return BodyModel
|
||||
|
||||
else:
|
||||
from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX
|
||||
from pydantic import AnyUrl as Url # noqa: F401
|
||||
from pydantic import ( # type: ignore[assignment]
|
||||
BaseConfig as BaseConfig, # noqa: F401
|
||||
)
|
||||
from pydantic import ValidationError as ValidationError # noqa: F401
|
||||
from pydantic.class_validators import ( # type: ignore[no-redef]
|
||||
Validator as Validator, # noqa: F401
|
||||
)
|
||||
from pydantic.error_wrappers import ( # type: ignore[no-redef]
|
||||
ErrorWrapper as ErrorWrapper, # noqa: F401
|
||||
)
|
||||
from pydantic.errors import MissingError
|
||||
from pydantic.fields import ( # type: ignore[attr-defined]
|
||||
SHAPE_FROZENSET,
|
||||
SHAPE_LIST,
|
||||
SHAPE_SEQUENCE,
|
||||
SHAPE_SET,
|
||||
SHAPE_SINGLETON,
|
||||
SHAPE_TUPLE,
|
||||
SHAPE_TUPLE_ELLIPSIS,
|
||||
)
|
||||
from pydantic.fields import FieldInfo as FieldInfo
|
||||
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
|
||||
ModelField as ModelField, # noqa: F401
|
||||
)
|
||||
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
|
||||
Required as Required, # noqa: F401
|
||||
)
|
||||
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
|
||||
Undefined as Undefined,
|
||||
)
|
||||
from pydantic.fields import ( # type: ignore[no-redef, attr-defined]
|
||||
UndefinedType as UndefinedType, # noqa: F401
|
||||
)
|
||||
from pydantic.networks import ( # type: ignore[no-redef]
|
||||
MultiHostDsn as MultiHostUrl, # noqa: F401
|
||||
)
|
||||
from pydantic.schema import (
|
||||
field_schema,
|
||||
get_flat_models_from_fields,
|
||||
get_model_name_map,
|
||||
model_process_schema,
|
||||
)
|
||||
from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401
|
||||
get_annotation_from_field_info as get_annotation_from_field_info,
|
||||
)
|
||||
from pydantic.typing import ( # type: ignore[no-redef]
|
||||
evaluate_forwardref as evaluate_forwardref, # noqa: F401
|
||||
)
|
||||
from pydantic.utils import ( # type: ignore[no-redef]
|
||||
lenient_issubclass as lenient_issubclass, # noqa: F401
|
||||
)
|
||||
|
||||
ErrorDetails = Dict[str, Any] # type: ignore[assignment,misc]
|
||||
GetJsonSchemaHandler = Any # type: ignore[assignment,misc]
|
||||
JsonSchemaValue = Dict[str, Any] # type: ignore[misc]
|
||||
CoreSchema = Any # type: ignore[assignment,misc]
|
||||
|
||||
sequence_shapes = {
|
||||
SHAPE_LIST,
|
||||
SHAPE_SET,
|
||||
SHAPE_FROZENSET,
|
||||
SHAPE_TUPLE,
|
||||
SHAPE_SEQUENCE,
|
||||
SHAPE_TUPLE_ELLIPSIS,
|
||||
}
|
||||
sequence_shape_to_type = {
|
||||
SHAPE_LIST: list,
|
||||
SHAPE_SET: set,
|
||||
SHAPE_TUPLE: tuple,
|
||||
SHAPE_SEQUENCE: list,
|
||||
SHAPE_TUPLE_ELLIPSIS: list,
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class GenerateJsonSchema: # type: ignore[no-redef]
|
||||
ref_template: str
|
||||
|
||||
class PydanticSchemaGenerationError(Exception): # type: ignore[no-redef]
|
||||
pass
|
||||
|
||||
def general_plain_validator_function( # type: ignore[misc]
|
||||
function: Callable[..., Any],
|
||||
*,
|
||||
ref: Union[str, None] = None,
|
||||
metadata: Any = None,
|
||||
serialization: Any = None,
|
||||
) -> Any:
|
||||
return {}
|
||||
|
||||
def get_model_definitions(
|
||||
*,
|
||||
flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
|
||||
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
|
||||
) -> Dict[str, Any]:
|
||||
definitions: Dict[str, Dict[str, Any]] = {}
|
||||
for model in flat_models:
|
||||
m_schema, m_definitions, m_nested_models = model_process_schema(
|
||||
model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)
|
||||
definitions.update(m_definitions)
|
||||
model_name = model_name_map[model]
|
||||
if "description" in m_schema:
|
||||
m_schema["description"] = m_schema["description"].split("\f")[0]
|
||||
definitions[model_name] = m_schema
|
||||
return definitions
|
||||
|
||||
def is_pv1_scalar_field(field: ModelField) -> bool:
|
||||
from fastapi import params
|
||||
|
||||
field_info = field.field_info
|
||||
if not (
|
||||
field.shape == SHAPE_SINGLETON # type: ignore[attr-defined]
|
||||
and not lenient_issubclass(field.type_, BaseModel)
|
||||
and not lenient_issubclass(field.type_, dict)
|
||||
and not field_annotation_is_sequence(field.type_)
|
||||
and not is_dataclass(field.type_)
|
||||
and not isinstance(field_info, params.Body)
|
||||
):
|
||||
return False
|
||||
if field.sub_fields: # type: ignore[attr-defined]
|
||||
if not all(
|
||||
is_pv1_scalar_field(f)
|
||||
for f in field.sub_fields # type: ignore[attr-defined]
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_pv1_scalar_sequence_field(field: ModelField) -> bool:
|
||||
if (field.shape in sequence_shapes) and not lenient_issubclass( # type: ignore[attr-defined]
|
||||
field.type_, BaseModel
|
||||
):
|
||||
if field.sub_fields is not None: # type: ignore[attr-defined]
|
||||
for sub_field in field.sub_fields: # type: ignore[attr-defined]
|
||||
if not is_pv1_scalar_field(sub_field):
|
||||
return False
|
||||
return True
|
||||
if _annotation_is_sequence(field.type_):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
|
||||
use_errors: List[Any] = []
|
||||
for error in errors:
|
||||
if isinstance(error, ErrorWrapper):
|
||||
new_errors = ValidationError( # type: ignore[call-arg]
|
||||
errors=[error], model=RequestErrorModel
|
||||
).errors()
|
||||
use_errors.extend(new_errors)
|
||||
elif isinstance(error, list):
|
||||
use_errors.extend(_normalize_errors(error))
|
||||
else:
|
||||
use_errors.append(error)
|
||||
return use_errors
|
||||
|
||||
def _model_rebuild(model: Type[BaseModel]) -> None:
|
||||
model.update_forward_refs()
|
||||
|
||||
def _model_dump(
|
||||
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
|
||||
) -> Any:
|
||||
return model.dict(**kwargs)
|
||||
|
||||
def _get_model_config(model: BaseModel) -> Any:
|
||||
return model.__config__ # type: ignore[attr-defined]
|
||||
|
||||
def get_schema_from_model_field(
|
||||
*,
|
||||
field: ModelField,
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
) -> Dict[str, Any]:
|
||||
# This expects that GenerateJsonSchema was already used to generate the definitions
|
||||
return field_schema( # type: ignore[no-any-return]
|
||||
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)[0]
|
||||
|
||||
def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
|
||||
models = get_flat_models_from_fields(fields, known_models=set())
|
||||
return get_model_name_map(models) # type: ignore[no-any-return]
|
||||
|
||||
def get_definitions(
|
||||
*,
|
||||
fields: List[ModelField],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
models = get_flat_models_from_fields(fields, known_models=set())
|
||||
return get_model_definitions(flat_models=models, model_name_map=model_name_map)
|
||||
|
||||
def is_scalar_field(field: ModelField) -> bool:
|
||||
return is_pv1_scalar_field(field)
|
||||
|
||||
def is_sequence_field(field: ModelField) -> bool:
|
||||
return field.shape in sequence_shapes or _annotation_is_sequence(field.type_) # type: ignore[attr-defined]
|
||||
|
||||
def is_scalar_sequence_field(field: ModelField) -> bool:
|
||||
return is_pv1_scalar_sequence_field(field)
|
||||
|
||||
def is_bytes_field(field: ModelField) -> bool:
|
||||
return lenient_issubclass(field.type_, bytes)
|
||||
|
||||
def is_bytes_sequence_field(field: ModelField) -> bool:
|
||||
return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) # type: ignore[attr-defined]
|
||||
|
||||
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
|
||||
return copy(field_info)
|
||||
|
||||
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
|
||||
return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return,attr-defined]
|
||||
|
||||
def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
|
||||
missing_field_error = ErrorWrapper(MissingError(), loc=loc) # type: ignore[call-arg]
|
||||
new_error = ValidationError([missing_field_error], RequestErrorModel)
|
||||
return new_error.errors()[0] # type: ignore[return-value]
|
||||
|
||||
def create_body_model(
|
||||
*, fields: Sequence[ModelField], model_name: str
|
||||
) -> Type[BaseModel]:
|
||||
BodyModel = create_model(model_name)
|
||||
for f in fields:
|
||||
BodyModel.__fields__[f.name] = f # type: ignore[index]
|
||||
return BodyModel
|
||||
|
||||
|
||||
def _regenerate_error_with_loc(
|
||||
*, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
|
||||
) -> List[Dict[str, Any]]:
|
||||
updated_loc_errors: List[Any] = [
|
||||
{**err, "loc": loc_prefix + err.get("loc", ())}
|
||||
for err in _normalize_errors(errors)
|
||||
]
|
||||
|
||||
return updated_loc_errors
|
||||
|
||||
|
||||
def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
|
||||
if lenient_issubclass(annotation, (str, bytes)):
|
||||
return False
|
||||
return lenient_issubclass(annotation, sequence_types)
|
||||
|
||||
|
||||
def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
|
||||
return _annotation_is_sequence(annotation) or _annotation_is_sequence(
|
||||
get_origin(annotation)
|
||||
)
|
||||
|
||||
|
||||
def value_is_sequence(value: Any) -> bool:
|
||||
return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
|
||||
return (
|
||||
lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile))
|
||||
or _annotation_is_sequence(annotation)
|
||||
or is_dataclass(annotation)
|
||||
)
|
||||
|
||||
|
||||
def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
return any(field_annotation_is_complex(arg) for arg in get_args(annotation))
|
||||
|
||||
return (
|
||||
_annotation_is_complex(annotation)
|
||||
or _annotation_is_complex(origin)
|
||||
or hasattr(origin, "__pydantic_core_schema__")
|
||||
or hasattr(origin, "__get_pydantic_core_schema__")
|
||||
)
|
||||
|
||||
|
||||
def field_annotation_is_scalar(annotation: Any) -> bool:
|
||||
# handle Ellipsis here to make tuple[int, ...] work nicely
|
||||
return annotation is Ellipsis or not field_annotation_is_complex(annotation)
|
||||
|
||||
|
||||
def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool:
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
at_least_one_scalar_sequence = False
|
||||
for arg in get_args(annotation):
|
||||
if field_annotation_is_scalar_sequence(arg):
|
||||
at_least_one_scalar_sequence = True
|
||||
continue
|
||||
elif not field_annotation_is_scalar(arg):
|
||||
return False
|
||||
return at_least_one_scalar_sequence
|
||||
return field_annotation_is_sequence(annotation) and all(
|
||||
field_annotation_is_scalar(sub_annotation)
|
||||
for sub_annotation in get_args(annotation)
|
||||
)
|
||||
|
||||
|
||||
def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool:
|
||||
if lenient_issubclass(annotation, bytes):
|
||||
return True
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
for arg in get_args(annotation):
|
||||
if lenient_issubclass(arg, bytes):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_uploadfile_or_nonable_uploadfile_annotation(annotation: Any) -> bool:
|
||||
if lenient_issubclass(annotation, UploadFile):
|
||||
return True
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
for arg in get_args(annotation):
|
||||
if lenient_issubclass(arg, UploadFile):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_bytes_sequence_annotation(annotation: Any) -> bool:
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
at_least_one = False
|
||||
for arg in get_args(annotation):
|
||||
if is_bytes_sequence_annotation(arg):
|
||||
at_least_one = True
|
||||
continue
|
||||
return at_least_one
|
||||
return field_annotation_is_sequence(annotation) and all(
|
||||
is_bytes_or_nonable_bytes_annotation(sub_annotation)
|
||||
for sub_annotation in get_args(annotation)
|
||||
)
|
||||
|
||||
|
||||
def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
at_least_one = False
|
||||
for arg in get_args(annotation):
|
||||
if is_uploadfile_sequence_annotation(arg):
|
||||
at_least_one = True
|
||||
continue
|
||||
return at_least_one
|
||||
return field_annotation_is_sequence(annotation) and all(
|
||||
is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation)
|
||||
for sub_annotation in get_args(annotation)
|
||||
)
|
||||
@@ -15,7 +15,6 @@ from typing import (
|
||||
|
||||
from fastapi import routing
|
||||
from fastapi.datastructures import Default, DefaultPlaceholder
|
||||
from fastapi.encoders import DictIntStrAny, SetIntStr
|
||||
from fastapi.exception_handlers import (
|
||||
http_exception_handler,
|
||||
request_validation_exception_handler,
|
||||
@@ -31,7 +30,7 @@ from fastapi.openapi.docs import (
|
||||
)
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from fastapi.params import Depends
|
||||
from fastapi.types import DecoratedCallable
|
||||
from fastapi.types import DecoratedCallable, IncEx
|
||||
from fastapi.utils import generate_unique_id
|
||||
from starlette.applications import Starlette
|
||||
from starlette.datastructures import State
|
||||
@@ -297,8 +296,8 @@ class FastAPI(Starlette):
|
||||
deprecated: Optional[bool] = None,
|
||||
methods: Optional[List[str]] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -355,8 +354,8 @@ class FastAPI(Starlette):
|
||||
deprecated: Optional[bool] = None,
|
||||
methods: Optional[List[str]] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -476,8 +475,8 @@ class FastAPI(Starlette):
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -531,8 +530,8 @@ class FastAPI(Starlette):
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -586,8 +585,8 @@ class FastAPI(Starlette):
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -641,8 +640,8 @@ class FastAPI(Starlette):
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -696,8 +695,8 @@ class FastAPI(Starlette):
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -751,8 +750,8 @@ class FastAPI(Starlette):
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -806,8 +805,8 @@ class FastAPI(Starlette):
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -861,8 +860,8 @@ class FastAPI(Starlette):
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
from typing import Any, Callable, Dict, Iterable, Type, TypeVar
|
||||
from typing import Any, Callable, Dict, Iterable, Type, TypeVar, cast
|
||||
|
||||
from fastapi._compat import (
|
||||
PYDANTIC_V2,
|
||||
CoreSchema,
|
||||
GetJsonSchemaHandler,
|
||||
JsonSchemaValue,
|
||||
general_plain_validator_function,
|
||||
)
|
||||
from starlette.datastructures import URL as URL # noqa: F401
|
||||
from starlette.datastructures import Address as Address # noqa: F401
|
||||
from starlette.datastructures import FormData as FormData # noqa: F401
|
||||
@@ -21,8 +28,28 @@ class UploadFile(StarletteUploadFile):
|
||||
return v
|
||||
|
||||
@classmethod
|
||||
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
|
||||
field_schema.update({"type": "string", "format": "binary"})
|
||||
def _validate(cls, __input_value: Any, _: Any) -> "UploadFile":
|
||||
if not isinstance(__input_value, StarletteUploadFile):
|
||||
raise ValueError(f"Expected UploadFile, received: {type(__input_value)}")
|
||||
return cast(UploadFile, __input_value)
|
||||
|
||||
if not PYDANTIC_V2:
|
||||
|
||||
@classmethod
|
||||
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
|
||||
field_schema.update({"type": "string", "format": "binary"})
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_json_schema__(
|
||||
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
|
||||
) -> JsonSchemaValue:
|
||||
return {"type": "string", "format": "binary"}
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
|
||||
) -> CoreSchema:
|
||||
return general_plain_validator_function(cls._validate)
|
||||
|
||||
|
||||
class DefaultPlaceholder:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Any, Callable, List, Optional, Sequence
|
||||
|
||||
from fastapi._compat import ModelField
|
||||
from fastapi.security.base import SecurityBase
|
||||
from pydantic.fields import ModelField
|
||||
|
||||
|
||||
class SecurityRequirement:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import dataclasses
|
||||
import inspect
|
||||
from contextlib import contextmanager
|
||||
from copy import copy, deepcopy
|
||||
from copy import deepcopy
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
@@ -20,6 +19,31 @@ from typing import (
|
||||
|
||||
import anyio
|
||||
from fastapi import params
|
||||
from fastapi._compat import (
|
||||
PYDANTIC_V2,
|
||||
ErrorWrapper,
|
||||
ModelField,
|
||||
Required,
|
||||
Undefined,
|
||||
_regenerate_error_with_loc,
|
||||
copy_field_info,
|
||||
create_body_model,
|
||||
evaluate_forwardref,
|
||||
field_annotation_is_scalar,
|
||||
get_annotation_from_field_info,
|
||||
get_missing_field_error,
|
||||
is_bytes_field,
|
||||
is_bytes_sequence_field,
|
||||
is_scalar_field,
|
||||
is_scalar_sequence_field,
|
||||
is_sequence_field,
|
||||
is_uploadfile_or_nonable_uploadfile_annotation,
|
||||
is_uploadfile_sequence_annotation,
|
||||
lenient_issubclass,
|
||||
sequence_types,
|
||||
serialize_sequence_value,
|
||||
value_is_sequence,
|
||||
)
|
||||
from fastapi.concurrency import (
|
||||
AsyncExitStack,
|
||||
asynccontextmanager,
|
||||
@@ -31,50 +55,14 @@ from fastapi.security.base import SecurityBase
|
||||
from fastapi.security.oauth2 import OAuth2, SecurityScopes
|
||||
from fastapi.security.open_id_connect_url import OpenIdConnect
|
||||
from fastapi.utils import create_response_field, get_path_param_names
|
||||
from pydantic import BaseModel, create_model
|
||||
from pydantic.error_wrappers import ErrorWrapper
|
||||
from pydantic.errors import MissingError
|
||||
from pydantic.fields import (
|
||||
SHAPE_FROZENSET,
|
||||
SHAPE_LIST,
|
||||
SHAPE_SEQUENCE,
|
||||
SHAPE_SET,
|
||||
SHAPE_SINGLETON,
|
||||
SHAPE_TUPLE,
|
||||
SHAPE_TUPLE_ELLIPSIS,
|
||||
FieldInfo,
|
||||
ModelField,
|
||||
Required,
|
||||
Undefined,
|
||||
)
|
||||
from pydantic.schema import get_annotation_from_field_info
|
||||
from pydantic.typing import evaluate_forwardref, get_args, get_origin
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from pydantic.fields import FieldInfo
|
||||
from starlette.background import BackgroundTasks
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
|
||||
from starlette.requests import HTTPConnection, Request
|
||||
from starlette.responses import Response
|
||||
from starlette.websockets import WebSocket
|
||||
from typing_extensions import Annotated
|
||||
|
||||
sequence_shapes = {
|
||||
SHAPE_LIST,
|
||||
SHAPE_SET,
|
||||
SHAPE_FROZENSET,
|
||||
SHAPE_TUPLE,
|
||||
SHAPE_SEQUENCE,
|
||||
SHAPE_TUPLE_ELLIPSIS,
|
||||
}
|
||||
sequence_types = (list, set, tuple)
|
||||
sequence_shape_to_type = {
|
||||
SHAPE_LIST: list,
|
||||
SHAPE_SET: set,
|
||||
SHAPE_TUPLE: tuple,
|
||||
SHAPE_SEQUENCE: list,
|
||||
SHAPE_TUPLE_ELLIPSIS: list,
|
||||
}
|
||||
|
||||
from typing_extensions import Annotated, get_args, get_origin
|
||||
|
||||
multipart_not_installed_error = (
|
||||
'Form data requires "python-multipart" to be installed. \n'
|
||||
@@ -216,36 +204,6 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]:
|
||||
)
|
||||
|
||||
|
||||
def is_scalar_field(field: ModelField) -> bool:
|
||||
field_info = field.field_info
|
||||
if not (
|
||||
field.shape == SHAPE_SINGLETON
|
||||
and not lenient_issubclass(field.type_, BaseModel)
|
||||
and not lenient_issubclass(field.type_, sequence_types + (dict,))
|
||||
and not dataclasses.is_dataclass(field.type_)
|
||||
and not isinstance(field_info, params.Body)
|
||||
):
|
||||
return False
|
||||
if field.sub_fields:
|
||||
if not all(is_scalar_field(f) for f in field.sub_fields):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def is_scalar_sequence_field(field: ModelField) -> bool:
|
||||
if (field.shape in sequence_shapes) and not lenient_issubclass(
|
||||
field.type_, BaseModel
|
||||
):
|
||||
if field.sub_fields is not None:
|
||||
for sub_field in field.sub_fields:
|
||||
if not is_scalar_field(sub_field):
|
||||
return False
|
||||
return True
|
||||
if lenient_issubclass(field.type_, sequence_types):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
||||
signature = inspect.signature(call)
|
||||
globalns = getattr(call, "__globals__", {})
|
||||
@@ -364,12 +322,11 @@ def analyze_param(
|
||||
is_path_param: bool,
|
||||
) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]:
|
||||
field_info = None
|
||||
used_default_field_info = False
|
||||
depends = None
|
||||
type_annotation: Any = Any
|
||||
if (
|
||||
annotation is not inspect.Signature.empty
|
||||
and get_origin(annotation) is Annotated # type: ignore[comparison-overlap]
|
||||
and get_origin(annotation) is Annotated
|
||||
):
|
||||
annotated_args = get_args(annotation)
|
||||
type_annotation = annotated_args[0]
|
||||
@@ -384,7 +341,9 @@ def analyze_param(
|
||||
fastapi_annotation = next(iter(fastapi_annotations), None)
|
||||
if isinstance(fastapi_annotation, FieldInfo):
|
||||
# Copy `field_info` because we mutate `field_info.default` below.
|
||||
field_info = copy(fastapi_annotation)
|
||||
field_info = copy_field_info(
|
||||
field_info=fastapi_annotation, annotation=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."
|
||||
@@ -415,6 +374,8 @@ def analyze_param(
|
||||
f" together for {param_name!r}"
|
||||
)
|
||||
field_info = value
|
||||
if PYDANTIC_V2:
|
||||
field_info.annotation = type_annotation
|
||||
|
||||
if depends is not None and depends.dependency is None:
|
||||
depends.dependency = type_annotation
|
||||
@@ -433,10 +394,15 @@ def analyze_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()
|
||||
field_info = params.Path(annotation=type_annotation)
|
||||
elif is_uploadfile_or_nonable_uploadfile_annotation(
|
||||
type_annotation
|
||||
) or is_uploadfile_sequence_annotation(type_annotation):
|
||||
field_info = params.File(annotation=type_annotation, default=default_value)
|
||||
elif not field_annotation_is_scalar(annotation=type_annotation):
|
||||
field_info = params.Body(annotation=type_annotation, default=default_value)
|
||||
else:
|
||||
field_info = params.Query(default=default_value)
|
||||
used_default_field_info = True
|
||||
field_info = params.Query(annotation=type_annotation, default=default_value)
|
||||
|
||||
field = None
|
||||
if field_info is not None:
|
||||
@@ -450,8 +416,8 @@ def analyze_param(
|
||||
and getattr(field_info, "in_", None) is None
|
||||
):
|
||||
field_info.in_ = params.ParamTypes.query
|
||||
annotation = get_annotation_from_field_info(
|
||||
annotation if annotation is not inspect.Signature.empty else Any,
|
||||
use_annotation = get_annotation_from_field_info(
|
||||
type_annotation,
|
||||
field_info,
|
||||
param_name,
|
||||
)
|
||||
@@ -459,19 +425,15 @@ def analyze_param(
|
||||
alias = param_name.replace("_", "-")
|
||||
else:
|
||||
alias = field_info.alias or param_name
|
||||
field_info.alias = alias
|
||||
field = create_response_field(
|
||||
name=param_name,
|
||||
type_=annotation,
|
||||
type_=use_annotation,
|
||||
default=field_info.default,
|
||||
alias=alias,
|
||||
required=field_info.default in (Required, Undefined),
|
||||
field_info=field_info,
|
||||
)
|
||||
if used_default_field_info:
|
||||
if lenient_issubclass(field.type_, UploadFile):
|
||||
field.field_info = params.File(field_info.default)
|
||||
elif not is_scalar_field(field=field):
|
||||
field.field_info = params.Body(field_info.default)
|
||||
|
||||
return type_annotation, depends, field
|
||||
|
||||
@@ -554,13 +516,13 @@ async def solve_dependencies(
|
||||
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
|
||||
) -> Tuple[
|
||||
Dict[str, Any],
|
||||
List[ErrorWrapper],
|
||||
List[Any],
|
||||
Optional[BackgroundTasks],
|
||||
Response,
|
||||
Dict[Tuple[Callable[..., Any], Tuple[str]], Any],
|
||||
]:
|
||||
values: Dict[str, Any] = {}
|
||||
errors: List[ErrorWrapper] = []
|
||||
errors: List[Any] = []
|
||||
if response is None:
|
||||
response = Response()
|
||||
del response.headers["content-length"]
|
||||
@@ -674,7 +636,7 @@ async def solve_dependencies(
|
||||
def request_params_to_args(
|
||||
required_params: Sequence[ModelField],
|
||||
received_params: Union[Mapping[str, Any], QueryParams, Headers],
|
||||
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
|
||||
) -> Tuple[Dict[str, Any], List[Any]]:
|
||||
values = {}
|
||||
errors = []
|
||||
for field in required_params:
|
||||
@@ -688,23 +650,19 @@ def request_params_to_args(
|
||||
assert isinstance(
|
||||
field_info, params.Param
|
||||
), "Params must be subclasses of Param"
|
||||
loc = (field_info.in_.value, field.alias)
|
||||
if value is None:
|
||||
if field.required:
|
||||
errors.append(
|
||||
ErrorWrapper(
|
||||
MissingError(), loc=(field_info.in_.value, field.alias)
|
||||
)
|
||||
)
|
||||
errors.append(get_missing_field_error(loc=loc))
|
||||
else:
|
||||
values[field.name] = deepcopy(field.default)
|
||||
continue
|
||||
v_, errors_ = field.validate(
|
||||
value, values, loc=(field_info.in_.value, field.alias)
|
||||
)
|
||||
v_, errors_ = field.validate(value, values, loc=loc)
|
||||
if isinstance(errors_, ErrorWrapper):
|
||||
errors.append(errors_)
|
||||
elif isinstance(errors_, list):
|
||||
errors.extend(errors_)
|
||||
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
|
||||
errors.extend(new_errors)
|
||||
else:
|
||||
values[field.name] = v_
|
||||
return values, errors
|
||||
@@ -713,9 +671,9 @@ def request_params_to_args(
|
||||
async def request_body_to_args(
|
||||
required_params: List[ModelField],
|
||||
received_body: Optional[Union[Dict[str, Any], FormData]],
|
||||
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
|
||||
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
|
||||
values = {}
|
||||
errors = []
|
||||
errors: List[Dict[str, Any]] = []
|
||||
if required_params:
|
||||
field = required_params[0]
|
||||
field_info = field.field_info
|
||||
@@ -733,9 +691,7 @@ async def request_body_to_args(
|
||||
|
||||
value: Optional[Any] = None
|
||||
if received_body is not None:
|
||||
if (
|
||||
field.shape in sequence_shapes or field.type_ in sequence_types
|
||||
) and isinstance(received_body, FormData):
|
||||
if (is_sequence_field(field)) and isinstance(received_body, FormData):
|
||||
value = received_body.getlist(field.alias)
|
||||
else:
|
||||
try:
|
||||
@@ -748,7 +704,7 @@ async def request_body_to_args(
|
||||
or (isinstance(field_info, params.Form) and value == "")
|
||||
or (
|
||||
isinstance(field_info, params.Form)
|
||||
and field.shape in sequence_shapes
|
||||
and is_sequence_field(field)
|
||||
and len(value) == 0
|
||||
)
|
||||
):
|
||||
@@ -759,16 +715,17 @@ async def request_body_to_args(
|
||||
continue
|
||||
if (
|
||||
isinstance(field_info, params.File)
|
||||
and lenient_issubclass(field.type_, bytes)
|
||||
and is_bytes_field(field)
|
||||
and isinstance(value, UploadFile)
|
||||
):
|
||||
value = await value.read()
|
||||
elif (
|
||||
field.shape in sequence_shapes
|
||||
is_bytes_sequence_field(field)
|
||||
and isinstance(field_info, params.File)
|
||||
and lenient_issubclass(field.type_, bytes)
|
||||
and isinstance(value, sequence_types)
|
||||
and value_is_sequence(value)
|
||||
):
|
||||
# For types
|
||||
assert isinstance(value, sequence_types) # type: ignore[arg-type]
|
||||
results: List[Union[bytes, str]] = []
|
||||
|
||||
async def process_fn(
|
||||
@@ -780,24 +737,19 @@ async def request_body_to_args(
|
||||
async with anyio.create_task_group() as tg:
|
||||
for sub_value in value:
|
||||
tg.start_soon(process_fn, sub_value.read)
|
||||
value = sequence_shape_to_type[field.shape](results)
|
||||
value = serialize_sequence_value(field=field, value=results)
|
||||
|
||||
v_, errors_ = field.validate(value, values, loc=loc)
|
||||
|
||||
if isinstance(errors_, ErrorWrapper):
|
||||
errors.append(errors_)
|
||||
elif isinstance(errors_, list):
|
||||
if isinstance(errors_, list):
|
||||
errors.extend(errors_)
|
||||
elif errors_:
|
||||
errors.append(errors_)
|
||||
else:
|
||||
values[field.name] = v_
|
||||
return values, errors
|
||||
|
||||
|
||||
def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper:
|
||||
missing_field_error = ErrorWrapper(MissingError(), loc=loc)
|
||||
return missing_field_error
|
||||
|
||||
|
||||
def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
|
||||
flat_dependant = get_flat_dependant(dependant)
|
||||
if not flat_dependant.body_params:
|
||||
@@ -815,12 +767,16 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
|
||||
for param in flat_dependant.body_params:
|
||||
setattr(param.field_info, "embed", True) # noqa: B010
|
||||
model_name = "Body_" + name
|
||||
BodyModel: Type[BaseModel] = create_model(model_name)
|
||||
for f in flat_dependant.body_params:
|
||||
BodyModel.__fields__[f.name] = f
|
||||
BodyModel = create_body_model(
|
||||
fields=flat_dependant.body_params, model_name=model_name
|
||||
)
|
||||
required = any(True for f in flat_dependant.body_params if f.required)
|
||||
|
||||
BodyFieldInfo_kwargs: Dict[str, Any] = {"default": None}
|
||||
BodyFieldInfo_kwargs: Dict[str, Any] = {
|
||||
"annotation": BodyModel,
|
||||
"alias": "body",
|
||||
}
|
||||
if not required:
|
||||
BodyFieldInfo_kwargs["default"] = None
|
||||
if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params):
|
||||
BodyFieldInfo: Type[params.Body] = params.File
|
||||
elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params):
|
||||
|
||||
@@ -1,15 +1,87 @@
|
||||
import dataclasses
|
||||
from collections import defaultdict
|
||||
import datetime
|
||||
from collections import defaultdict, deque
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from pathlib import PurePath
|
||||
from ipaddress import (
|
||||
IPv4Address,
|
||||
IPv4Interface,
|
||||
IPv4Network,
|
||||
IPv6Address,
|
||||
IPv6Interface,
|
||||
IPv6Network,
|
||||
)
|
||||
from pathlib import Path, PurePath
|
||||
from re import Pattern
|
||||
from types import GeneratorType
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi.types import IncEx
|
||||
from pydantic import BaseModel
|
||||
from pydantic.json import ENCODERS_BY_TYPE
|
||||
from pydantic.color import Color
|
||||
from pydantic.networks import NameEmail
|
||||
from pydantic.types import SecretBytes, SecretStr
|
||||
|
||||
SetIntStr = Set[Union[int, str]]
|
||||
DictIntStrAny = Dict[Union[int, str], Any]
|
||||
from ._compat import PYDANTIC_V2, MultiHostUrl, Url, _model_dump
|
||||
|
||||
|
||||
# Taken from Pydantic v1 as is
|
||||
def isoformat(o: Union[datetime.date, datetime.time]) -> str:
|
||||
return o.isoformat()
|
||||
|
||||
|
||||
# Taken from Pydantic v1 as is
|
||||
# TODO: pv2 should this return strings instead?
|
||||
def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
|
||||
"""
|
||||
Encodes a Decimal as int of there's no exponent, otherwise float
|
||||
|
||||
This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
|
||||
where a integer (but not int typed) is used. Encoding this as a float
|
||||
results in failed round-tripping between encode and parse.
|
||||
Our Id type is a prime example of this.
|
||||
|
||||
>>> decimal_encoder(Decimal("1.0"))
|
||||
1.0
|
||||
|
||||
>>> decimal_encoder(Decimal("1"))
|
||||
1
|
||||
"""
|
||||
if dec_value.as_tuple().exponent >= 0: # type: ignore[operator]
|
||||
return int(dec_value)
|
||||
else:
|
||||
return float(dec_value)
|
||||
|
||||
|
||||
ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
|
||||
bytes: lambda o: o.decode(),
|
||||
Color: str,
|
||||
datetime.date: isoformat,
|
||||
datetime.datetime: isoformat,
|
||||
datetime.time: isoformat,
|
||||
datetime.timedelta: lambda td: td.total_seconds(),
|
||||
Decimal: decimal_encoder,
|
||||
Enum: lambda o: o.value,
|
||||
frozenset: list,
|
||||
deque: list,
|
||||
GeneratorType: list,
|
||||
IPv4Address: str,
|
||||
IPv4Interface: str,
|
||||
IPv4Network: str,
|
||||
IPv6Address: str,
|
||||
IPv6Interface: str,
|
||||
IPv6Network: str,
|
||||
NameEmail: str,
|
||||
Path: str,
|
||||
Pattern: lambda o: o.pattern,
|
||||
SecretBytes: str,
|
||||
SecretStr: str,
|
||||
set: list,
|
||||
UUID: str,
|
||||
Url: str,
|
||||
MultiHostUrl: str,
|
||||
}
|
||||
|
||||
|
||||
def generate_encoders_by_class_tuples(
|
||||
@@ -28,8 +100,8 @@ encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)
|
||||
|
||||
def jsonable_encoder(
|
||||
obj: Any,
|
||||
include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
include: Optional[IncEx] = None,
|
||||
exclude: Optional[IncEx] = None,
|
||||
by_alias: bool = True,
|
||||
exclude_unset: bool = False,
|
||||
exclude_defaults: bool = False,
|
||||
@@ -50,10 +122,15 @@ def jsonable_encoder(
|
||||
if exclude is not None and not isinstance(exclude, (set, dict)):
|
||||
exclude = set(exclude)
|
||||
if isinstance(obj, BaseModel):
|
||||
encoder = getattr(obj.__config__, "json_encoders", {})
|
||||
if custom_encoder:
|
||||
encoder.update(custom_encoder)
|
||||
obj_dict = obj.dict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
encoders: Dict[Any, Any] = {}
|
||||
if not PYDANTIC_V2:
|
||||
encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined]
|
||||
if custom_encoder:
|
||||
encoders.update(custom_encoder)
|
||||
obj_dict = _model_dump(
|
||||
obj,
|
||||
mode="json",
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
@@ -67,7 +144,8 @@ def jsonable_encoder(
|
||||
obj_dict,
|
||||
exclude_none=exclude_none,
|
||||
exclude_defaults=exclude_defaults,
|
||||
custom_encoder=encoder,
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
custom_encoder=encoders,
|
||||
sqlalchemy_safe=sqlalchemy_safe,
|
||||
)
|
||||
if dataclasses.is_dataclass(obj):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from typing import Any, Dict, Optional, Sequence, Type
|
||||
|
||||
from pydantic import BaseModel, ValidationError, create_model
|
||||
from pydantic.error_wrappers import ErrorList
|
||||
from pydantic import BaseModel, create_model
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.exceptions import WebSocketException as WebSocketException # noqa: F401
|
||||
|
||||
@@ -26,12 +25,25 @@ class FastAPIError(RuntimeError):
|
||||
"""
|
||||
|
||||
|
||||
class RequestValidationError(ValidationError):
|
||||
def __init__(self, errors: Sequence[ErrorList], *, body: Any = None) -> None:
|
||||
class ValidationException(Exception):
|
||||
def __init__(self, errors: Sequence[Any]) -> None:
|
||||
self._errors = errors
|
||||
|
||||
def errors(self) -> Sequence[Any]:
|
||||
return self._errors
|
||||
|
||||
|
||||
class RequestValidationError(ValidationException):
|
||||
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
|
||||
super().__init__(errors)
|
||||
self.body = body
|
||||
super().__init__(errors, RequestErrorModel)
|
||||
|
||||
|
||||
class WebSocketRequestValidationError(ValidationError):
|
||||
def __init__(self, errors: Sequence[ErrorList]) -> None:
|
||||
super().__init__(errors, WebSocketErrorModel)
|
||||
class WebSocketRequestValidationError(ValidationException):
|
||||
pass
|
||||
|
||||
|
||||
class ResponseValidationError(ValidationException):
|
||||
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
|
||||
super().__init__(errors)
|
||||
self.body = body
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"}
|
||||
REF_PREFIX = "#/components/schemas/"
|
||||
REF_TEMPLATE = "#/components/schemas/{model}"
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Union
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Type, Union
|
||||
|
||||
from fastapi._compat import (
|
||||
PYDANTIC_V2,
|
||||
CoreSchema,
|
||||
GetJsonSchemaHandler,
|
||||
JsonSchemaValue,
|
||||
_model_rebuild,
|
||||
general_plain_validator_function,
|
||||
)
|
||||
from fastapi.logger import logger
|
||||
from pydantic import AnyUrl, BaseModel, Field
|
||||
from typing_extensions import Literal
|
||||
|
||||
try:
|
||||
import email_validator # type: ignore
|
||||
import email_validator
|
||||
|
||||
assert email_validator # make autoflake ignore the unused import
|
||||
from pydantic import EmailStr
|
||||
@@ -25,22 +33,52 @@ except ImportError: # pragma: no cover
|
||||
)
|
||||
return str(v)
|
||||
|
||||
@classmethod
|
||||
def _validate(cls, __input_value: Any, _: Any) -> str:
|
||||
logger.warning(
|
||||
"email-validator not installed, email fields will be treated as str.\n"
|
||||
"To install, run: pip install email-validator"
|
||||
)
|
||||
return str(__input_value)
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_json_schema__(
|
||||
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
|
||||
) -> JsonSchemaValue:
|
||||
return {"type": "string", "format": "email"}
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
|
||||
) -> CoreSchema:
|
||||
return general_plain_validator_function(cls._validate)
|
||||
|
||||
|
||||
class Contact(BaseModel):
|
||||
name: Optional[str] = None
|
||||
url: Optional[AnyUrl] = None
|
||||
email: Optional[EmailStr] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class License(BaseModel):
|
||||
name: str
|
||||
url: Optional[AnyUrl] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Info(BaseModel):
|
||||
@@ -51,8 +89,13 @@ class Info(BaseModel):
|
||||
license: Optional[License] = None
|
||||
version: str
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class ServerVariable(BaseModel):
|
||||
@@ -60,8 +103,13 @@ class ServerVariable(BaseModel):
|
||||
default: str
|
||||
description: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Server(BaseModel):
|
||||
@@ -69,8 +117,13 @@ class Server(BaseModel):
|
||||
description: Optional[str] = None
|
||||
variables: Optional[Dict[str, ServerVariable]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Reference(BaseModel):
|
||||
@@ -89,16 +142,26 @@ class XML(BaseModel):
|
||||
attribute: Optional[bool] = None
|
||||
wrapped: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class ExternalDocumentation(BaseModel):
|
||||
description: Optional[str] = None
|
||||
url: AnyUrl
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Schema(BaseModel):
|
||||
@@ -139,8 +202,13 @@ class Schema(BaseModel):
|
||||
example: Optional[Any] = None
|
||||
deprecated: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
extra: str = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Example(BaseModel):
|
||||
@@ -149,8 +217,13 @@ class Example(BaseModel):
|
||||
value: Optional[Any] = None
|
||||
externalValue: Optional[AnyUrl] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class ParameterInType(Enum):
|
||||
@@ -167,8 +240,13 @@ class Encoding(BaseModel):
|
||||
explode: Optional[bool] = None
|
||||
allowReserved: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class MediaType(BaseModel):
|
||||
@@ -177,8 +255,13 @@ class MediaType(BaseModel):
|
||||
examples: Optional[Dict[str, Union[Example, Reference]]] = None
|
||||
encoding: Optional[Dict[str, Encoding]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class ParameterBase(BaseModel):
|
||||
@@ -195,8 +278,13 @@ class ParameterBase(BaseModel):
|
||||
# Serialization rules for more complex scenarios
|
||||
content: Optional[Dict[str, MediaType]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Parameter(ParameterBase):
|
||||
@@ -213,8 +301,13 @@ class RequestBody(BaseModel):
|
||||
content: Dict[str, MediaType]
|
||||
required: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Link(BaseModel):
|
||||
@@ -225,8 +318,13 @@ class Link(BaseModel):
|
||||
description: Optional[str] = None
|
||||
server: Optional[Server] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Response(BaseModel):
|
||||
@@ -235,8 +333,13 @@ class Response(BaseModel):
|
||||
content: Optional[Dict[str, MediaType]] = None
|
||||
links: Optional[Dict[str, Union[Link, Reference]]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Operation(BaseModel):
|
||||
@@ -254,8 +357,13 @@ class Operation(BaseModel):
|
||||
security: Optional[List[Dict[str, List[str]]]] = None
|
||||
servers: Optional[List[Server]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class PathItem(BaseModel):
|
||||
@@ -273,8 +381,13 @@ class PathItem(BaseModel):
|
||||
servers: Optional[List[Server]] = None
|
||||
parameters: Optional[List[Union[Parameter, Reference]]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class SecuritySchemeType(Enum):
|
||||
@@ -288,8 +401,13 @@ class SecurityBase(BaseModel):
|
||||
type_: SecuritySchemeType = Field(alias="type")
|
||||
description: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class APIKeyIn(Enum):
|
||||
@@ -318,8 +436,13 @@ class OAuthFlow(BaseModel):
|
||||
refreshUrl: Optional[str] = None
|
||||
scopes: Dict[str, str] = {}
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class OAuthFlowImplicit(OAuthFlow):
|
||||
@@ -345,8 +468,13 @@ class OAuthFlows(BaseModel):
|
||||
clientCredentials: Optional[OAuthFlowClientCredentials] = None
|
||||
authorizationCode: Optional[OAuthFlowAuthorizationCode] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class OAuth2(SecurityBase):
|
||||
@@ -376,8 +504,13 @@ class Components(BaseModel):
|
||||
# Using Any for Specification Extensions
|
||||
callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Tag(BaseModel):
|
||||
@@ -385,8 +518,13 @@ class Tag(BaseModel):
|
||||
description: Optional[str] = None
|
||||
externalDocs: Optional[ExternalDocumentation] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class OpenAPI(BaseModel):
|
||||
@@ -400,10 +538,15 @@ class OpenAPI(BaseModel):
|
||||
tags: Optional[List[Tag]] = None
|
||||
externalDocs: Optional[ExternalDocumentation] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
Schema.update_forward_refs()
|
||||
Operation.update_forward_refs()
|
||||
Encoding.update_forward_refs()
|
||||
_model_rebuild(Schema)
|
||||
_model_rebuild(Operation)
|
||||
_model_rebuild(Encoding)
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import http.client
|
||||
import inspect
|
||||
import warnings
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast
|
||||
|
||||
from fastapi import routing
|
||||
from fastapi._compat import (
|
||||
GenerateJsonSchema,
|
||||
ModelField,
|
||||
Undefined,
|
||||
get_compat_model_name_map,
|
||||
get_definitions,
|
||||
get_schema_from_model_field,
|
||||
lenient_issubclass,
|
||||
)
|
||||
from fastapi.datastructures import DefaultPlaceholder
|
||||
from fastapi.dependencies.models import Dependant
|
||||
from fastapi.dependencies.utils import get_flat_dependant, get_flat_params
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX
|
||||
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE
|
||||
from fastapi.openapi.models import OpenAPI
|
||||
from fastapi.params import Body, Param
|
||||
from fastapi.responses import Response
|
||||
from fastapi.types import ModelNameMap
|
||||
from fastapi.utils import (
|
||||
deep_dict_update,
|
||||
generate_operation_id_for_path,
|
||||
get_model_definitions,
|
||||
is_body_allowed_for_status_code,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import ModelField, Undefined
|
||||
from pydantic.schema import (
|
||||
field_schema,
|
||||
get_flat_models_from_fields,
|
||||
get_model_name_map,
|
||||
)
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.routing import BaseRoute
|
||||
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
|
||||
@@ -88,7 +88,8 @@ def get_openapi_security_definitions(
|
||||
def get_openapi_operation_parameters(
|
||||
*,
|
||||
all_route_params: Sequence[ModelField],
|
||||
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
) -> List[Dict[str, Any]]:
|
||||
parameters = []
|
||||
for param in all_route_params:
|
||||
@@ -96,13 +97,16 @@ def get_openapi_operation_parameters(
|
||||
field_info = cast(Param, field_info)
|
||||
if not field_info.include_in_schema:
|
||||
continue
|
||||
param_schema = get_schema_from_model_field(
|
||||
field=param,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
)
|
||||
parameter = {
|
||||
"name": param.alias,
|
||||
"in": field_info.in_.value,
|
||||
"required": param.required,
|
||||
"schema": field_schema(
|
||||
param, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)[0],
|
||||
"schema": param_schema,
|
||||
}
|
||||
if field_info.description:
|
||||
parameter["description"] = field_info.description
|
||||
@@ -119,13 +123,16 @@ def get_openapi_operation_parameters(
|
||||
def get_openapi_operation_request_body(
|
||||
*,
|
||||
body_field: Optional[ModelField],
|
||||
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if not body_field:
|
||||
return None
|
||||
assert isinstance(body_field, ModelField)
|
||||
body_schema, _, _ = field_schema(
|
||||
body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
body_schema = get_schema_from_model_field(
|
||||
field=body_field,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
)
|
||||
field_info = cast(Body, body_field.field_info)
|
||||
request_media_type = field_info.media_type
|
||||
@@ -190,7 +197,11 @@ def get_openapi_operation_metadata(
|
||||
|
||||
|
||||
def get_openapi_path(
|
||||
*, route: routing.APIRoute, model_name_map: Dict[type, str], operation_ids: Set[str]
|
||||
*,
|
||||
route: routing.APIRoute,
|
||||
operation_ids: Set[str],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
|
||||
path = {}
|
||||
security_schemes: Dict[str, Any] = {}
|
||||
@@ -218,7 +229,9 @@ def get_openapi_path(
|
||||
security_schemes.update(security_definitions)
|
||||
all_route_params = get_flat_params(route.dependant)
|
||||
operation_parameters = get_openapi_operation_parameters(
|
||||
all_route_params=all_route_params, model_name_map=model_name_map
|
||||
all_route_params=all_route_params,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
)
|
||||
parameters.extend(operation_parameters)
|
||||
if parameters:
|
||||
@@ -236,7 +249,9 @@ def get_openapi_path(
|
||||
operation["parameters"] = list(all_parameters.values())
|
||||
if method in METHODS_WITH_BODY:
|
||||
request_body_oai = get_openapi_operation_request_body(
|
||||
body_field=route.body_field, model_name_map=model_name_map
|
||||
body_field=route.body_field,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
)
|
||||
if request_body_oai:
|
||||
operation["requestBody"] = request_body_oai
|
||||
@@ -250,8 +265,9 @@ def get_openapi_path(
|
||||
cb_definitions,
|
||||
) = get_openapi_path(
|
||||
route=callback,
|
||||
model_name_map=model_name_map,
|
||||
operation_ids=operation_ids,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
)
|
||||
callbacks[callback.name] = {callback.path: cb_path}
|
||||
operation["callbacks"] = callbacks
|
||||
@@ -277,10 +293,10 @@ def get_openapi_path(
|
||||
response_schema = {"type": "string"}
|
||||
if lenient_issubclass(current_response_class, JSONResponse):
|
||||
if route.response_field:
|
||||
response_schema, _, _ = field_schema(
|
||||
route.response_field,
|
||||
response_schema = get_schema_from_model_field(
|
||||
field=route.response_field,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
ref_prefix=REF_PREFIX,
|
||||
)
|
||||
else:
|
||||
response_schema = {}
|
||||
@@ -309,8 +325,10 @@ def get_openapi_path(
|
||||
field = route.response_fields.get(additional_status_code)
|
||||
additional_field_schema: Optional[Dict[str, Any]] = None
|
||||
if field:
|
||||
additional_field_schema, _, _ = field_schema(
|
||||
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
additional_field_schema = get_schema_from_model_field(
|
||||
field=field,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
)
|
||||
media_type = route_response_media_type or "application/json"
|
||||
additional_schema = (
|
||||
@@ -356,13 +374,13 @@ def get_openapi_path(
|
||||
return path, security_schemes, definitions
|
||||
|
||||
|
||||
def get_flat_models_from_routes(
|
||||
def get_fields_from_routes(
|
||||
routes: Sequence[BaseRoute],
|
||||
) -> Set[Union[Type[BaseModel], Type[Enum]]]:
|
||||
) -> List[ModelField]:
|
||||
body_fields_from_routes: List[ModelField] = []
|
||||
responses_from_routes: List[ModelField] = []
|
||||
request_fields_from_routes: List[ModelField] = []
|
||||
callback_flat_models: Set[Union[Type[BaseModel], Type[Enum]]] = set()
|
||||
callback_flat_models: List[ModelField] = []
|
||||
for route in routes:
|
||||
if getattr(route, "include_in_schema", None) and isinstance(
|
||||
route, routing.APIRoute
|
||||
@@ -377,13 +395,12 @@ def get_flat_models_from_routes(
|
||||
if route.response_fields:
|
||||
responses_from_routes.extend(route.response_fields.values())
|
||||
if route.callbacks:
|
||||
callback_flat_models |= get_flat_models_from_routes(route.callbacks)
|
||||
callback_flat_models.extend(get_fields_from_routes(route.callbacks))
|
||||
params = get_flat_params(route.dependant)
|
||||
request_fields_from_routes.extend(params)
|
||||
|
||||
flat_models = callback_flat_models | get_flat_models_from_fields(
|
||||
body_fields_from_routes + responses_from_routes + request_fields_from_routes,
|
||||
known_models=set(),
|
||||
flat_models = callback_flat_models + list(
|
||||
body_fields_from_routes + responses_from_routes + request_fields_from_routes
|
||||
)
|
||||
return flat_models
|
||||
|
||||
@@ -416,15 +433,21 @@ def get_openapi(
|
||||
components: Dict[str, Dict[str, Any]] = {}
|
||||
paths: Dict[str, Dict[str, Any]] = {}
|
||||
operation_ids: Set[str] = set()
|
||||
flat_models = get_flat_models_from_routes(routes)
|
||||
model_name_map = get_model_name_map(flat_models)
|
||||
definitions = get_model_definitions(
|
||||
flat_models=flat_models, model_name_map=model_name_map
|
||||
all_fields = get_fields_from_routes(routes)
|
||||
model_name_map = get_compat_model_name_map(all_fields)
|
||||
schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
|
||||
definitions = get_definitions(
|
||||
fields=all_fields,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
)
|
||||
for route in routes:
|
||||
if isinstance(route, routing.APIRoute):
|
||||
result = get_openapi_path(
|
||||
route=route, model_name_map=model_name_map, operation_ids=operation_ids
|
||||
route=route,
|
||||
operation_ids=operation_ids,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
)
|
||||
if result:
|
||||
path, security_schemes, path_definitions = result
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Any, Callable, Dict, Optional, Sequence
|
||||
|
||||
from fastapi import params
|
||||
from pydantic.fields import Undefined
|
||||
from fastapi._compat import Undefined
|
||||
|
||||
|
||||
def Path( # noqa: N802
|
||||
@@ -16,6 +16,7 @@ def Path( # noqa: N802
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
example: Any = Undefined,
|
||||
examples: Optional[Dict[str, Any]] = None,
|
||||
@@ -34,6 +35,7 @@ def Path( # noqa: N802
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
@@ -55,6 +57,7 @@ def Query( # noqa: N802
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
example: Any = Undefined,
|
||||
examples: Optional[Dict[str, Any]] = None,
|
||||
@@ -73,6 +76,7 @@ def Query( # noqa: N802
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
@@ -95,6 +99,7 @@ def Header( # noqa: N802
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
example: Any = Undefined,
|
||||
examples: Optional[Dict[str, Any]] = None,
|
||||
@@ -114,6 +119,7 @@ def Header( # noqa: N802
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
@@ -135,6 +141,7 @@ def Cookie( # noqa: N802
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
example: Any = Undefined,
|
||||
examples: Optional[Dict[str, Any]] = None,
|
||||
@@ -153,6 +160,7 @@ def Cookie( # noqa: N802
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
@@ -176,6 +184,7 @@ def Body( # noqa: N802
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
example: Any = Undefined,
|
||||
examples: Optional[Dict[str, Any]] = None,
|
||||
@@ -194,6 +203,7 @@ def Body( # noqa: N802
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
@@ -214,6 +224,7 @@ def Form( # noqa: N802
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
example: Any = Undefined,
|
||||
examples: Optional[Dict[str, Any]] = None,
|
||||
@@ -231,6 +242,7 @@ def Form( # noqa: N802
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
@@ -251,6 +263,7 @@ def File( # noqa: N802
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
example: Any = Undefined,
|
||||
examples: Optional[Dict[str, Any]] = None,
|
||||
@@ -268,6 +281,7 @@ def File( # noqa: N802
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, Optional, Sequence
|
||||
from typing import Any, Callable, Dict, Optional, Sequence, Type
|
||||
|
||||
from pydantic.fields import FieldInfo, Undefined
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
from ._compat import PYDANTIC_V2, Undefined
|
||||
|
||||
|
||||
class ParamTypes(Enum):
|
||||
@@ -18,6 +20,7 @@ class Param(FieldInfo):
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
annotation: Optional[Type[Any]] = None,
|
||||
alias: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
@@ -27,6 +30,7 @@ class Param(FieldInfo):
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
example: Any = Undefined,
|
||||
examples: Optional[Dict[str, Any]] = None,
|
||||
@@ -36,9 +40,8 @@ class Param(FieldInfo):
|
||||
):
|
||||
self.deprecated = deprecated
|
||||
self.example = example
|
||||
self.examples = examples
|
||||
self.include_in_schema = include_in_schema
|
||||
super().__init__(
|
||||
kwargs = dict(
|
||||
default=default,
|
||||
alias=alias,
|
||||
title=title,
|
||||
@@ -49,9 +52,19 @@ class Param(FieldInfo):
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
regex=regex,
|
||||
**extra,
|
||||
)
|
||||
if PYDANTIC_V2:
|
||||
kwargs["annotation"] = annotation
|
||||
kwargs["pattern"] = pattern or regex
|
||||
else:
|
||||
# TODO: pv2 figure out how to deprecate regex
|
||||
kwargs["regex"] = pattern or regex
|
||||
|
||||
super().__init__(**kwargs)
|
||||
# TODO: pv2 decide how to handle OpenAPI examples vs JSON Schema examples
|
||||
# and how to deprecate OpenAPI examples
|
||||
self.examples = examples # type: ignore[assignment]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.default})"
|
||||
@@ -64,6 +77,7 @@ class Path(Param):
|
||||
self,
|
||||
default: Any = ...,
|
||||
*,
|
||||
annotation: Optional[Type[Any]] = None,
|
||||
alias: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
@@ -73,6 +87,7 @@ class Path(Param):
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
example: Any = Undefined,
|
||||
examples: Optional[Dict[str, Any]] = None,
|
||||
@@ -84,6 +99,7 @@ class Path(Param):
|
||||
self.in_ = self.in_
|
||||
super().__init__(
|
||||
default=default,
|
||||
annotation=annotation,
|
||||
alias=alias,
|
||||
title=title,
|
||||
description=description,
|
||||
@@ -93,6 +109,7 @@ class Path(Param):
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
@@ -109,6 +126,7 @@ class Query(Param):
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
annotation: Optional[Type[Any]] = None,
|
||||
alias: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
@@ -118,6 +136,7 @@ class Query(Param):
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
example: Any = Undefined,
|
||||
examples: Optional[Dict[str, Any]] = None,
|
||||
@@ -127,6 +146,7 @@ class Query(Param):
|
||||
):
|
||||
super().__init__(
|
||||
default=default,
|
||||
annotation=annotation,
|
||||
alias=alias,
|
||||
title=title,
|
||||
description=description,
|
||||
@@ -136,6 +156,7 @@ class Query(Param):
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
@@ -152,6 +173,7 @@ class Header(Param):
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
annotation: Optional[Type[Any]] = None,
|
||||
alias: Optional[str] = None,
|
||||
convert_underscores: bool = True,
|
||||
title: Optional[str] = None,
|
||||
@@ -162,6 +184,7 @@ class Header(Param):
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
example: Any = Undefined,
|
||||
examples: Optional[Dict[str, Any]] = None,
|
||||
@@ -172,6 +195,7 @@ class Header(Param):
|
||||
self.convert_underscores = convert_underscores
|
||||
super().__init__(
|
||||
default=default,
|
||||
annotation=annotation,
|
||||
alias=alias,
|
||||
title=title,
|
||||
description=description,
|
||||
@@ -181,6 +205,7 @@ class Header(Param):
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
@@ -197,6 +222,7 @@ class Cookie(Param):
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
annotation: Optional[Type[Any]] = None,
|
||||
alias: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
@@ -206,6 +232,7 @@ class Cookie(Param):
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
example: Any = Undefined,
|
||||
examples: Optional[Dict[str, Any]] = None,
|
||||
@@ -215,6 +242,7 @@ class Cookie(Param):
|
||||
):
|
||||
super().__init__(
|
||||
default=default,
|
||||
annotation=annotation,
|
||||
alias=alias,
|
||||
title=title,
|
||||
description=description,
|
||||
@@ -224,6 +252,7 @@ class Cookie(Param):
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
@@ -238,6 +267,7 @@ class Body(FieldInfo):
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
annotation: Optional[Type[Any]] = None,
|
||||
embed: bool = False,
|
||||
media_type: str = "application/json",
|
||||
alias: Optional[str] = None,
|
||||
@@ -249,6 +279,7 @@ class Body(FieldInfo):
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
example: Any = Undefined,
|
||||
examples: Optional[Dict[str, Any]] = None,
|
||||
@@ -257,8 +288,7 @@ class Body(FieldInfo):
|
||||
self.embed = embed
|
||||
self.media_type = media_type
|
||||
self.example = example
|
||||
self.examples = examples
|
||||
super().__init__(
|
||||
kwargs = dict(
|
||||
default=default,
|
||||
alias=alias,
|
||||
title=title,
|
||||
@@ -269,9 +299,20 @@ class Body(FieldInfo):
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
regex=regex,
|
||||
**extra,
|
||||
)
|
||||
if PYDANTIC_V2:
|
||||
kwargs["annotation"] = annotation
|
||||
kwargs["pattern"] = pattern or regex
|
||||
else:
|
||||
# TODO: pv2 figure out how to deprecate regex
|
||||
kwargs["regex"] = pattern or regex
|
||||
super().__init__(
|
||||
**kwargs,
|
||||
)
|
||||
# TODO: pv2 decide how to handle OpenAPI examples vs JSON Schema examples
|
||||
# and how to deprecate OpenAPI examples
|
||||
self.examples = examples # type: ignore[assignment]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.default})"
|
||||
@@ -282,6 +323,7 @@ class Form(Body):
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
annotation: Optional[Type[Any]] = None,
|
||||
media_type: str = "application/x-www-form-urlencoded",
|
||||
alias: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
@@ -292,6 +334,7 @@ class Form(Body):
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
example: Any = Undefined,
|
||||
examples: Optional[Dict[str, Any]] = None,
|
||||
@@ -299,6 +342,7 @@ class Form(Body):
|
||||
):
|
||||
super().__init__(
|
||||
default=default,
|
||||
annotation=annotation,
|
||||
embed=True,
|
||||
media_type=media_type,
|
||||
alias=alias,
|
||||
@@ -310,6 +354,7 @@ class Form(Body):
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
@@ -322,6 +367,7 @@ class File(Form):
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
annotation: Optional[Type[Any]] = None,
|
||||
media_type: str = "multipart/form-data",
|
||||
alias: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
@@ -332,6 +378,7 @@ class File(Form):
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
example: Any = Undefined,
|
||||
examples: Optional[Dict[str, Any]] = None,
|
||||
@@ -339,6 +386,7 @@ class File(Form):
|
||||
):
|
||||
super().__init__(
|
||||
default=default,
|
||||
annotation=annotation,
|
||||
media_type=media_type,
|
||||
alias=alias,
|
||||
title=title,
|
||||
@@ -349,6 +397,7 @@ class File(Form):
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
example=example,
|
||||
examples=examples,
|
||||
|
||||
@@ -20,6 +20,14 @@ from typing import (
|
||||
)
|
||||
|
||||
from fastapi import params
|
||||
from fastapi._compat import (
|
||||
ModelField,
|
||||
Undefined,
|
||||
_get_model_config,
|
||||
_model_dump,
|
||||
_normalize_errors,
|
||||
lenient_issubclass,
|
||||
)
|
||||
from fastapi.datastructures import Default, DefaultPlaceholder
|
||||
from fastapi.dependencies.models import Dependant
|
||||
from fastapi.dependencies.utils import (
|
||||
@@ -29,13 +37,14 @@ from fastapi.dependencies.utils import (
|
||||
get_typed_return_annotation,
|
||||
solve_dependencies,
|
||||
)
|
||||
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.exceptions import (
|
||||
FastAPIError,
|
||||
RequestValidationError,
|
||||
ResponseValidationError,
|
||||
WebSocketRequestValidationError,
|
||||
)
|
||||
from fastapi.types import DecoratedCallable
|
||||
from fastapi.types import DecoratedCallable, IncEx
|
||||
from fastapi.utils import (
|
||||
create_cloned_field,
|
||||
create_response_field,
|
||||
@@ -44,9 +53,6 @@ from fastapi.utils import (
|
||||
is_body_allowed_for_status_code,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from pydantic.error_wrappers import ErrorWrapper, ValidationError
|
||||
from pydantic.fields import ModelField, Undefined
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from starlette import routing
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.exceptions import HTTPException
|
||||
@@ -73,14 +79,15 @@ def _prepare_response_content(
|
||||
exclude_none: bool = False,
|
||||
) -> Any:
|
||||
if isinstance(res, BaseModel):
|
||||
read_with_orm_mode = getattr(res.__config__, "read_with_orm_mode", None)
|
||||
read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None)
|
||||
if read_with_orm_mode:
|
||||
# Let from_orm extract the data from this model instead of converting
|
||||
# it now to a dict.
|
||||
# Otherwise there's no way to extract lazy data that requires attribute
|
||||
# access instead of dict iteration, e.g. lazy relationships.
|
||||
return res
|
||||
return res.dict(
|
||||
return _model_dump(
|
||||
res,
|
||||
by_alias=True,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
@@ -115,8 +122,8 @@ async def serialize_response(
|
||||
*,
|
||||
field: Optional[ModelField] = None,
|
||||
response_content: Any,
|
||||
include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
include: Optional[IncEx] = None,
|
||||
exclude: Optional[IncEx] = None,
|
||||
by_alias: bool = True,
|
||||
exclude_unset: bool = False,
|
||||
exclude_defaults: bool = False,
|
||||
@@ -125,24 +132,40 @@ async def serialize_response(
|
||||
) -> Any:
|
||||
if field:
|
||||
errors = []
|
||||
response_content = _prepare_response_content(
|
||||
response_content,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
if not hasattr(field, "serialize"):
|
||||
# pydantic v1
|
||||
response_content = _prepare_response_content(
|
||||
response_content,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
if is_coroutine:
|
||||
value, errors_ = field.validate(response_content, {}, loc=("response",))
|
||||
else:
|
||||
value, errors_ = await run_in_threadpool(
|
||||
field.validate, response_content, {}, loc=("response",)
|
||||
)
|
||||
if isinstance(errors_, ErrorWrapper):
|
||||
errors.append(errors_)
|
||||
elif isinstance(errors_, list):
|
||||
if isinstance(errors_, list):
|
||||
errors.extend(errors_)
|
||||
elif errors_:
|
||||
errors.append(errors_)
|
||||
if errors:
|
||||
raise ValidationError(errors, field.type_)
|
||||
raise ResponseValidationError(
|
||||
errors=_normalize_errors(errors), body=response_content
|
||||
)
|
||||
|
||||
if hasattr(field, "serialize"):
|
||||
return field.serialize(
|
||||
value,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
|
||||
return jsonable_encoder(
|
||||
value,
|
||||
include=include,
|
||||
@@ -175,8 +198,8 @@ def get_request_handler(
|
||||
status_code: Optional[int] = None,
|
||||
response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse),
|
||||
response_field: Optional[ModelField] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -220,7 +243,16 @@ def get_request_handler(
|
||||
body = body_bytes
|
||||
except json.JSONDecodeError as e:
|
||||
raise RequestValidationError(
|
||||
[ErrorWrapper(e, ("body", e.pos))], body=e.doc
|
||||
[
|
||||
{
|
||||
"type": "json_invalid",
|
||||
"loc": ("body", e.pos),
|
||||
"msg": "JSON decode error",
|
||||
"input": {},
|
||||
"ctx": {"error": e.msg},
|
||||
}
|
||||
],
|
||||
body=e.doc,
|
||||
) from e
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -236,7 +268,7 @@ def get_request_handler(
|
||||
)
|
||||
values, errors, background_tasks, sub_response, _ = solved_result
|
||||
if errors:
|
||||
raise RequestValidationError(errors, body=body)
|
||||
raise RequestValidationError(_normalize_errors(errors), body=body)
|
||||
else:
|
||||
raw_response = await run_endpoint_function(
|
||||
dependant=dependant, values=values, is_coroutine=is_coroutine
|
||||
@@ -287,7 +319,7 @@ def get_websocket_app(
|
||||
)
|
||||
values, errors, _, _2, _3 = solved_result
|
||||
if errors:
|
||||
raise WebSocketRequestValidationError(errors)
|
||||
raise WebSocketRequestValidationError(_normalize_errors(errors))
|
||||
assert dependant.call is not None, "dependant.call must be a function"
|
||||
await dependant.call(**values)
|
||||
|
||||
@@ -348,8 +380,8 @@ class APIRoute(routing.Route):
|
||||
name: Optional[str] = None,
|
||||
methods: Optional[Union[Set[str], List[str]]] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -423,6 +455,7 @@ class APIRoute(routing.Route):
|
||||
# would pass the validation and be returned as is.
|
||||
# By being a new field, no inheritance will be passed as is. A new model
|
||||
# will be always created.
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
self.secure_cloned_response_field: Optional[
|
||||
ModelField
|
||||
] = create_cloned_field(self.response_field)
|
||||
@@ -569,8 +602,8 @@ class APIRouter(routing.Router):
|
||||
deprecated: Optional[bool] = None,
|
||||
methods: Optional[Union[Set[str], List[str]]] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -650,8 +683,8 @@ class APIRouter(routing.Router):
|
||||
deprecated: Optional[bool] = None,
|
||||
methods: Optional[List[str]] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -877,8 +910,8 @@ class APIRouter(routing.Router):
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -933,8 +966,8 @@ class APIRouter(routing.Router):
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -989,8 +1022,8 @@ class APIRouter(routing.Router):
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -1045,8 +1078,8 @@ class APIRouter(routing.Router):
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -1101,8 +1134,8 @@ class APIRouter(routing.Router):
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -1157,8 +1190,8 @@ class APIRouter(routing.Router):
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -1213,8 +1246,8 @@ class APIRouter(routing.Router):
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
@@ -1269,8 +1302,8 @@ class APIRouter(routing.Router):
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
deprecated: Optional[bool] = None,
|
||||
operation_id: Optional[str] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_include: Optional[IncEx] = None,
|
||||
response_model_exclude: Optional[IncEx] = None,
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_exclude_unset: bool = False,
|
||||
response_model_exclude_defaults: bool = False,
|
||||
|
||||
@@ -9,6 +9,9 @@ from fastapi.security.utils import get_authorization_scheme_param
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
|
||||
|
||||
# TODO: import from typing when deprecating Python 3.9
|
||||
from typing_extensions import Annotated
|
||||
|
||||
|
||||
class OAuth2PasswordRequestForm:
|
||||
"""
|
||||
@@ -45,12 +48,13 @@ class OAuth2PasswordRequestForm:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
grant_type: str = Form(default=None, regex="password"),
|
||||
username: str = Form(),
|
||||
password: str = Form(),
|
||||
scope: str = Form(default=""),
|
||||
client_id: Optional[str] = Form(default=None),
|
||||
client_secret: Optional[str] = Form(default=None),
|
||||
*,
|
||||
grant_type: Annotated[Union[str, None], Form(pattern="password")] = None,
|
||||
username: Annotated[str, Form()],
|
||||
password: Annotated[str, Form()],
|
||||
scope: Annotated[str, Form()] = "",
|
||||
client_id: Annotated[Union[str, None], Form()] = None,
|
||||
client_secret: Annotated[Union[str, None], Form()] = None,
|
||||
):
|
||||
self.grant_type = grant_type
|
||||
self.username = username
|
||||
@@ -95,12 +99,12 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
grant_type: str = Form(regex="password"),
|
||||
username: str = Form(),
|
||||
password: str = Form(),
|
||||
scope: str = Form(default=""),
|
||||
client_id: Optional[str] = Form(default=None),
|
||||
client_secret: Optional[str] = Form(default=None),
|
||||
grant_type: Annotated[str, Form(pattern="password")],
|
||||
username: Annotated[str, Form()],
|
||||
password: Annotated[str, Form()],
|
||||
scope: Annotated[str, Form()] = "",
|
||||
client_id: Annotated[Union[str, None], Form()] = None,
|
||||
client_secret: Annotated[Union[str, None], Form()] = None,
|
||||
):
|
||||
super().__init__(
|
||||
grant_type=grant_type,
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
from typing import Any, Callable, TypeVar
|
||||
import types
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, Set, Type, TypeVar, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any])
|
||||
UnionType = getattr(types, "UnionType", Union)
|
||||
NoneType = getattr(types, "UnionType", None)
|
||||
ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str]
|
||||
IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]]
|
||||
|
||||
122
fastapi/utils.py
122
fastapi/utils.py
@@ -1,7 +1,6 @@
|
||||
import re
|
||||
import warnings
|
||||
from dataclasses import is_dataclass
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@@ -16,13 +15,19 @@ from typing import (
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
import fastapi
|
||||
from fastapi._compat import (
|
||||
PYDANTIC_V2,
|
||||
BaseConfig,
|
||||
ModelField,
|
||||
PydanticSchemaGenerationError,
|
||||
Undefined,
|
||||
UndefinedType,
|
||||
Validator,
|
||||
lenient_issubclass,
|
||||
)
|
||||
from fastapi.datastructures import DefaultPlaceholder, DefaultType
|
||||
from fastapi.openapi.constants import REF_PREFIX
|
||||
from pydantic import BaseConfig, BaseModel, create_model
|
||||
from pydantic.class_validators import Validator
|
||||
from pydantic.fields import FieldInfo, ModelField, UndefinedType
|
||||
from pydantic.schema import model_process_schema
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from pydantic import BaseModel, create_model
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
if TYPE_CHECKING: # pragma: nocover
|
||||
from .routing import APIRoute
|
||||
@@ -50,24 +55,6 @@ def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool:
|
||||
return not (current_status_code < 200 or current_status_code in {204, 304})
|
||||
|
||||
|
||||
def get_model_definitions(
|
||||
*,
|
||||
flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
|
||||
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
|
||||
) -> Dict[str, Any]:
|
||||
definitions: Dict[str, Dict[str, Any]] = {}
|
||||
for model in flat_models:
|
||||
m_schema, m_definitions, m_nested_models = model_process_schema(
|
||||
model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)
|
||||
definitions.update(m_definitions)
|
||||
model_name = model_name_map[model]
|
||||
if "description" in m_schema:
|
||||
m_schema["description"] = m_schema["description"].split("\f")[0]
|
||||
definitions[model_name] = m_schema
|
||||
return definitions
|
||||
|
||||
|
||||
def get_path_param_names(path: str) -> Set[str]:
|
||||
return set(re.findall("{(.*?)}", path))
|
||||
|
||||
@@ -76,8 +63,8 @@ def create_response_field(
|
||||
name: str,
|
||||
type_: Type[Any],
|
||||
class_validators: Optional[Dict[str, Validator]] = None,
|
||||
default: Optional[Any] = None,
|
||||
required: Union[bool, UndefinedType] = True,
|
||||
default: Optional[Any] = Undefined,
|
||||
required: Union[bool, UndefinedType] = Undefined,
|
||||
model_config: Type[BaseConfig] = BaseConfig,
|
||||
field_info: Optional[FieldInfo] = None,
|
||||
alias: Optional[str] = None,
|
||||
@@ -86,20 +73,27 @@ def create_response_field(
|
||||
Create a new response field. Raises if type_ is invalid.
|
||||
"""
|
||||
class_validators = class_validators or {}
|
||||
field_info = field_info or FieldInfo()
|
||||
|
||||
try:
|
||||
return ModelField(
|
||||
name=name,
|
||||
type_=type_,
|
||||
class_validators=class_validators,
|
||||
default=default,
|
||||
required=required,
|
||||
model_config=model_config,
|
||||
alias=alias,
|
||||
field_info=field_info,
|
||||
if PYDANTIC_V2:
|
||||
field_info = field_info or FieldInfo(
|
||||
annotation=type_, default=default, alias=alias
|
||||
)
|
||||
except RuntimeError:
|
||||
else:
|
||||
field_info = field_info or FieldInfo()
|
||||
kwargs = {"name": name, "field_info": field_info}
|
||||
if not PYDANTIC_V2:
|
||||
kwargs.update(
|
||||
{
|
||||
"type_": type_,
|
||||
"class_validators": class_validators,
|
||||
"default": default,
|
||||
"required": required,
|
||||
"model_config": model_config,
|
||||
"alias": alias,
|
||||
}
|
||||
)
|
||||
try:
|
||||
return ModelField(**kwargs) # type: ignore[arg-type]
|
||||
except (RuntimeError, PydanticSchemaGenerationError):
|
||||
raise fastapi.exceptions.FastAPIError(
|
||||
"Invalid args for response field! Hint: "
|
||||
f"check that {type_} is a valid Pydantic field type. "
|
||||
@@ -116,6 +110,8 @@ def create_cloned_field(
|
||||
*,
|
||||
cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None,
|
||||
) -> ModelField:
|
||||
if PYDANTIC_V2:
|
||||
return field
|
||||
# cloned_types caches already cloned types to support recursive models and improve
|
||||
# performance by avoiding unecessary cloning
|
||||
if cloned_types is None:
|
||||
@@ -136,30 +132,30 @@ def create_cloned_field(
|
||||
f, cloned_types=cloned_types
|
||||
)
|
||||
new_field = create_response_field(name=field.name, type_=use_type)
|
||||
new_field.has_alias = field.has_alias
|
||||
new_field.alias = field.alias
|
||||
new_field.class_validators = field.class_validators
|
||||
new_field.default = field.default
|
||||
new_field.required = field.required
|
||||
new_field.model_config = field.model_config
|
||||
new_field.has_alias = field.has_alias # type: ignore[attr-defined]
|
||||
new_field.alias = field.alias # type: ignore[misc]
|
||||
new_field.class_validators = field.class_validators # type: ignore[attr-defined]
|
||||
new_field.default = field.default # type: ignore[misc]
|
||||
new_field.required = field.required # type: ignore[misc]
|
||||
new_field.model_config = field.model_config # type: ignore[attr-defined]
|
||||
new_field.field_info = field.field_info
|
||||
new_field.allow_none = field.allow_none
|
||||
new_field.validate_always = field.validate_always
|
||||
if field.sub_fields:
|
||||
new_field.sub_fields = [
|
||||
new_field.allow_none = field.allow_none # type: ignore[attr-defined]
|
||||
new_field.validate_always = field.validate_always # type: ignore[attr-defined]
|
||||
if field.sub_fields: # type: ignore[attr-defined]
|
||||
new_field.sub_fields = [ # type: ignore[attr-defined]
|
||||
create_cloned_field(sub_field, cloned_types=cloned_types)
|
||||
for sub_field in field.sub_fields
|
||||
for sub_field in field.sub_fields # type: ignore[attr-defined]
|
||||
]
|
||||
if field.key_field:
|
||||
new_field.key_field = create_cloned_field(
|
||||
field.key_field, cloned_types=cloned_types
|
||||
if field.key_field: # type: ignore[attr-defined]
|
||||
new_field.key_field = create_cloned_field( # type: ignore[attr-defined]
|
||||
field.key_field, cloned_types=cloned_types # type: ignore[attr-defined]
|
||||
)
|
||||
new_field.validators = field.validators
|
||||
new_field.pre_validators = field.pre_validators
|
||||
new_field.post_validators = field.post_validators
|
||||
new_field.parse_json = field.parse_json
|
||||
new_field.shape = field.shape
|
||||
new_field.populate_validators()
|
||||
new_field.validators = field.validators # type: ignore[attr-defined]
|
||||
new_field.pre_validators = field.pre_validators # type: ignore[attr-defined]
|
||||
new_field.post_validators = field.post_validators # type: ignore[attr-defined]
|
||||
new_field.parse_json = field.parse_json # type: ignore[attr-defined]
|
||||
new_field.shape = field.shape # type: ignore[attr-defined]
|
||||
new_field.populate_validators() # type: ignore[attr-defined]
|
||||
return new_field
|
||||
|
||||
|
||||
@@ -220,3 +216,9 @@ def get_value_or_default(
|
||||
if not isinstance(item, DefaultPlaceholder):
|
||||
return item
|
||||
return first_item
|
||||
|
||||
|
||||
def match_pydantic_error_url(error_type: str) -> Any:
|
||||
from dirty_equals import IsStr
|
||||
|
||||
return IsStr(regex=rf"^https://errors\.pydantic\.dev/.*/v/{error_type}")
|
||||
|
||||
@@ -42,7 +42,7 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"starlette>=0.27.0,<0.28.0",
|
||||
"pydantic>=1.7.4,!=1.8,!=1.8.1,<2.0.0",
|
||||
"pydantic>=1.7.4,!=1.8,!=1.8.1,<3.0.0",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
@@ -59,7 +59,7 @@ all = [
|
||||
"pyyaml >=5.3.1",
|
||||
"ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0",
|
||||
"orjson >=3.2.1",
|
||||
"email_validator >=1.1.1",
|
||||
"email_validator >=2.0.0",
|
||||
"uvicorn[standard] >=0.12.0",
|
||||
]
|
||||
|
||||
@@ -83,6 +83,7 @@ check_untyped_defs = true
|
||||
addopts = [
|
||||
"--strict-config",
|
||||
"--strict-markers",
|
||||
"--ignore=docs_src",
|
||||
]
|
||||
xfail_strict = true
|
||||
junit_family = "xunit2"
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
-e .
|
||||
pytest >=7.1.3,<8.0.0
|
||||
coverage[toml] >= 6.5.0,< 8.0
|
||||
dirty-equals >= 0.6.0
|
||||
|
||||
mypy ==1.3.0
|
||||
ruff ==0.0.272
|
||||
black == 23.3.0
|
||||
httpx >=0.23.0,<0.24.0
|
||||
email_validator >=1.1.1,<2.0.0
|
||||
email_validator >=2.0.0,<3.0.0
|
||||
# TODO: once removing databases from tutorial, upgrade SQLAlchemy
|
||||
# probably when including SQLModel
|
||||
sqlalchemy >=1.3.18,<1.4.43
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
@@ -42,13 +43,24 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "Callback Url",
|
||||
"maxLength": 2083,
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"title": "Callback Url",
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
}
|
||||
)
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
| IsDict(
|
||||
{
|
||||
"title": "Callback Url",
|
||||
"maxLength": 2083,
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
}
|
||||
),
|
||||
"name": "callback_url",
|
||||
"in": "query",
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import APIRouter, FastAPI, Query
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
from typing_extensions import Annotated
|
||||
|
||||
app = FastAPI()
|
||||
@@ -30,21 +32,46 @@ client = TestClient(app)
|
||||
|
||||
foo_is_missing = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "foo"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
IsDict(
|
||||
{
|
||||
"loc": ["query", "foo"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
)
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
| IsDict(
|
||||
{
|
||||
"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",
|
||||
}
|
||||
IsDict(
|
||||
{
|
||||
"ctx": {"min_length": 1},
|
||||
"loc": ["query", "foo"],
|
||||
"msg": "String should have at least 1 characters",
|
||||
"type": "string_too_short",
|
||||
"input": "",
|
||||
"url": match_pydantic_error_url("string_too_short"),
|
||||
}
|
||||
)
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
| IsDict(
|
||||
{
|
||||
"ctx": {"limit_value": 1},
|
||||
"loc": ["query", "foo"],
|
||||
"msg": "ensure this value has at least 1 characters",
|
||||
"type": "value_error.any_str.min_length",
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from .main import app
|
||||
@@ -266,10 +267,17 @@ def test_openapi_schema():
|
||||
"operationId": "get_path_param_id_path_param__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
"required": True,
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Item Id",
|
||||
}
|
||||
)
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
| IsDict({"title": "Item Id", "type": "string"}),
|
||||
}
|
||||
],
|
||||
}
|
||||
@@ -969,10 +977,17 @@ def test_openapi_schema():
|
||||
"operationId": "get_query_type_optional_query_int_optional_get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Query", "type": "integer"},
|
||||
"name": "query",
|
||||
"in": "query",
|
||||
"required": False,
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"title": "Query",
|
||||
}
|
||||
)
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
| IsDict({"title": "Query", "type": "integer"}),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
93
tests/test_compat.py
Normal file
93
tests/test_compat.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from typing import List, Union
|
||||
|
||||
from fastapi import FastAPI, UploadFile
|
||||
from fastapi._compat import (
|
||||
ModelField,
|
||||
Undefined,
|
||||
_get_model_config,
|
||||
is_bytes_sequence_annotation,
|
||||
is_uploadfile_sequence_annotation,
|
||||
)
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseConfig, BaseModel, ConfigDict
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
from .utils import needs_pydanticv1, needs_pydanticv2
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_model_field_default_required():
|
||||
# For coverage
|
||||
field_info = FieldInfo(annotation=str)
|
||||
field = ModelField(name="foo", field_info=field_info)
|
||||
assert field.default is Undefined
|
||||
|
||||
|
||||
@needs_pydanticv1
|
||||
def test_upload_file_dummy_general_plain_validator_function():
|
||||
# For coverage
|
||||
assert UploadFile.__get_pydantic_core_schema__(str, lambda x: None) == {}
|
||||
|
||||
|
||||
@needs_pydanticv1
|
||||
def test_union_scalar_list():
|
||||
# For coverage
|
||||
# TODO: there might not be a current valid code path that uses this, it would
|
||||
# potentially enable query parameters defined as both a scalar and a list
|
||||
# but that would require more refactors, also not sure it's really useful
|
||||
from fastapi._compat import is_pv1_scalar_field
|
||||
|
||||
field_info = FieldInfo()
|
||||
field = ModelField(
|
||||
name="foo",
|
||||
field_info=field_info,
|
||||
type_=Union[str, List[int]],
|
||||
class_validators={},
|
||||
model_config=BaseConfig,
|
||||
)
|
||||
assert not is_pv1_scalar_field(field)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_get_model_config():
|
||||
# For coverage in Pydantic v2
|
||||
class Foo(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
foo = Foo()
|
||||
config = _get_model_config(foo)
|
||||
assert config == {"from_attributes": True}
|
||||
|
||||
|
||||
def test_complex():
|
||||
app = FastAPI()
|
||||
|
||||
@app.post("/")
|
||||
def foo(foo: Union[str, List[int]]):
|
||||
return foo
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post("/", json="bar")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == "bar"
|
||||
|
||||
response2 = client.post("/", json=[1, 2])
|
||||
assert response2.status_code == 200, response2.text
|
||||
assert response2.json() == [1, 2]
|
||||
|
||||
|
||||
def test_is_bytes_sequence_annotation_union():
|
||||
# For coverage
|
||||
# TODO: in theory this would allow declaring types that could be lists of bytes
|
||||
# to be read from files and other types, but I'm not even sure it's a good idea
|
||||
# to support it as a first class "feature"
|
||||
assert is_bytes_sequence_annotation(Union[List[str], List[bytes]])
|
||||
|
||||
|
||||
def test_is_uploadfile_sequence_annotation():
|
||||
# For coverage
|
||||
# TODO: in theory this would allow declaring types that could be lists of UploadFile
|
||||
# and other types, but I'm not even sure it's a good idea to support it as a first
|
||||
# class "feature"
|
||||
assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]])
|
||||
@@ -1,4 +1,5 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -8,10 +9,18 @@ app = FastAPI()
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"x-something-internal": {"level": 4},
|
||||
if PYDANTIC_V2:
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"x-something-internal": {"level": 4},
|
||||
}
|
||||
}
|
||||
else:
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"x-something-internal": {"level": 4},
|
||||
}
|
||||
|
||||
|
||||
@app.get("/foo", response_model=Item)
|
||||
|
||||
@@ -7,11 +7,17 @@ from fastapi.datastructures import Default
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
def test_upload_file_invalid():
|
||||
with pytest.raises(ValueError):
|
||||
UploadFile.validate("not a Starlette UploadFile")
|
||||
|
||||
|
||||
def test_upload_file_invalid_pydantic_v2():
|
||||
with pytest.raises(ValueError):
|
||||
UploadFile._validate("not a Starlette UploadFile", {})
|
||||
|
||||
|
||||
def test_default_placeholder_equals():
|
||||
placeholder_1 = Default("a")
|
||||
placeholder_2 = Default("a")
|
||||
|
||||
@@ -4,31 +4,54 @@ from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ModelWithDatetimeField(BaseModel):
|
||||
dt_field: datetime
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda dt: dt.replace(
|
||||
microsecond=0, tzinfo=timezone.utc
|
||||
).isoformat()
|
||||
}
|
||||
from .utils import needs_pydanticv1, needs_pydanticv2
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
|
||||
@needs_pydanticv2
|
||||
def test_pydanticv2():
|
||||
from pydantic import field_serializer
|
||||
|
||||
class ModelWithDatetimeField(BaseModel):
|
||||
dt_field: datetime
|
||||
|
||||
@app.get("/model", response_model=ModelWithDatetimeField)
|
||||
def get_model():
|
||||
return model
|
||||
@field_serializer("dt_field")
|
||||
def serialize_datetime(self, dt_field: datetime):
|
||||
return dt_field.replace(microsecond=0, tzinfo=timezone.utc).isoformat()
|
||||
|
||||
app = FastAPI()
|
||||
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
|
||||
|
||||
client = TestClient(app)
|
||||
@app.get("/model", response_model=ModelWithDatetimeField)
|
||||
def get_model():
|
||||
return model
|
||||
|
||||
|
||||
def test_dt():
|
||||
client = TestClient(app)
|
||||
with client:
|
||||
response = client.get("/model")
|
||||
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}
|
||||
|
||||
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
@needs_pydanticv1
|
||||
def test_pydanticv1():
|
||||
class ModelWithDatetimeField(BaseModel):
|
||||
dt_field: datetime
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda dt: dt.replace(
|
||||
microsecond=0, tzinfo=timezone.utc
|
||||
).isoformat()
|
||||
}
|
||||
|
||||
app = FastAPI()
|
||||
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
|
||||
|
||||
@app.get("/model", response_model=ModelWithDatetimeField)
|
||||
def get_model():
|
||||
return model
|
||||
|
||||
client = TestClient(app)
|
||||
with client:
|
||||
response = client.get("/model")
|
||||
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from typing import List
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
@@ -47,15 +49,30 @@ async def no_duplicates_sub(
|
||||
def test_no_duplicates_invalid():
|
||||
response = client.post("/no-duplicates", json={"item": {"data": "myitem"}})
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item2"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "item2"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item2"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_no_duplicates():
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import APIRouter, Depends, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@@ -50,99 +52,180 @@ async def overrider_dependency_with_sub(msg: dict = Depends(overrider_sub_depend
|
||||
return msg
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url,status_code,expected",
|
||||
[
|
||||
(
|
||||
"/main-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/main-depends/?q=foo",
|
||||
200,
|
||||
{"in": "main-depends", "params": {"q": "foo", "skip": 0, "limit": 100}},
|
||||
),
|
||||
(
|
||||
"/main-depends/?q=foo&skip=100&limit=200",
|
||||
200,
|
||||
{"in": "main-depends", "params": {"q": "foo", "skip": 100, "limit": 200}},
|
||||
),
|
||||
(
|
||||
"/decorator-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
("/decorator-depends/?q=foo", 200, {"in": "decorator-depends"}),
|
||||
(
|
||||
"/decorator-depends/?q=foo&skip=100&limit=200",
|
||||
200,
|
||||
{"in": "decorator-depends"},
|
||||
),
|
||||
(
|
||||
"/router-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/router-depends/?q=foo",
|
||||
200,
|
||||
{"in": "router-depends", "params": {"q": "foo", "skip": 0, "limit": 100}},
|
||||
),
|
||||
(
|
||||
"/router-depends/?q=foo&skip=100&limit=200",
|
||||
200,
|
||||
{"in": "router-depends", "params": {"q": "foo", "skip": 100, "limit": 200}},
|
||||
),
|
||||
(
|
||||
"/router-decorator-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
("/router-decorator-depends/?q=foo", 200, {"in": "router-decorator-depends"}),
|
||||
(
|
||||
"/router-decorator-depends/?q=foo&skip=100&limit=200",
|
||||
200,
|
||||
{"in": "router-decorator-depends"},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_normal_app(url, status_code, expected):
|
||||
response = client.get(url)
|
||||
assert response.status_code == status_code
|
||||
assert response.json() == expected
|
||||
def test_main_depends():
|
||||
response = client.get("/main-depends/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "q"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_main_depends_q_foo():
|
||||
response = client.get("/main-depends/?q=foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"in": "main-depends",
|
||||
"params": {"q": "foo", "skip": 0, "limit": 100},
|
||||
}
|
||||
|
||||
|
||||
def test_main_depends_q_foo_skip_100_limit_200():
|
||||
response = client.get("/main-depends/?q=foo&skip=100&limit=200")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"in": "main-depends",
|
||||
"params": {"q": "foo", "skip": 100, "limit": 200},
|
||||
}
|
||||
|
||||
|
||||
def test_decorator_depends():
|
||||
response = client.get("/decorator-depends/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "q"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_decorator_depends_q_foo():
|
||||
response = client.get("/decorator-depends/?q=foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"in": "decorator-depends"}
|
||||
|
||||
|
||||
def test_decorator_depends_q_foo_skip_100_limit_200():
|
||||
response = client.get("/decorator-depends/?q=foo&skip=100&limit=200")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"in": "decorator-depends"}
|
||||
|
||||
|
||||
def test_router_depends():
|
||||
response = client.get("/router-depends/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "q"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_router_depends_q_foo():
|
||||
response = client.get("/router-depends/?q=foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"in": "router-depends",
|
||||
"params": {"q": "foo", "skip": 0, "limit": 100},
|
||||
}
|
||||
|
||||
|
||||
def test_router_depends_q_foo_skip_100_limit_200():
|
||||
response = client.get("/router-depends/?q=foo&skip=100&limit=200")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"in": "router-depends",
|
||||
"params": {"q": "foo", "skip": 100, "limit": 200},
|
||||
}
|
||||
|
||||
|
||||
def test_router_decorator_depends():
|
||||
response = client.get("/router-decorator-depends/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "q"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_router_decorator_depends_q_foo():
|
||||
response = client.get("/router-decorator-depends/?q=foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"in": "router-decorator-depends"}
|
||||
|
||||
|
||||
def test_router_decorator_depends_q_foo_skip_100_limit_200():
|
||||
response = client.get("/router-decorator-depends/?q=foo&skip=100&limit=200")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"in": "router-decorator-depends"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -190,126 +273,281 @@ def test_override_simple(url, status_code, expected):
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url,status_code,expected",
|
||||
[
|
||||
(
|
||||
"/main-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/main-depends/?q=foo",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
("/main-depends/?k=bar", 200, {"in": "main-depends", "params": {"k": "bar"}}),
|
||||
(
|
||||
"/decorator-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/decorator-depends/?q=foo",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
("/decorator-depends/?k=bar", 200, {"in": "decorator-depends"}),
|
||||
(
|
||||
"/router-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/router-depends/?q=foo",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/router-depends/?k=bar",
|
||||
200,
|
||||
{"in": "router-depends", "params": {"k": "bar"}},
|
||||
),
|
||||
(
|
||||
"/router-decorator-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/router-decorator-depends/?q=foo",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
("/router-decorator-depends/?k=bar", 200, {"in": "router-decorator-depends"}),
|
||||
],
|
||||
)
|
||||
def test_override_with_sub(url, status_code, expected):
|
||||
def test_override_with_sub_main_depends():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get(url)
|
||||
assert response.status_code == status_code
|
||||
assert response.json() == expected
|
||||
response = client.get("/main-depends/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "k"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub__main_depends_q_foo():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/main-depends/?q=foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "k"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_main_depends_k_bar():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/main-depends/?k=bar")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"in": "main-depends", "params": {"k": "bar"}}
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_decorator_depends():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/decorator-depends/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "k"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_decorator_depends_q_foo():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/decorator-depends/?q=foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "k"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_decorator_depends_k_bar():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/decorator-depends/?k=bar")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"in": "decorator-depends"}
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_router_depends():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/router-depends/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "k"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_router_depends_q_foo():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/router-depends/?q=foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "k"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_router_depends_k_bar():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/router-depends/?k=bar")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"in": "router-depends", "params": {"k": "bar"}}
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_router_decorator_depends():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/router-decorator-depends/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "k"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_router_decorator_depends_q_foo():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/router-decorator-depends/?q=foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "k"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_override_with_sub_router_decorator_depends_k_bar():
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get("/router-decorator-depends/?k=bar")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"in": "router-decorator-depends"}
|
||||
app.dependency_overrides = {}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -327,7 +328,14 @@ def test_openapi_schema():
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"price": IsDict(
|
||||
{
|
||||
"title": "Price",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
| IsDict({"title": "Price", "type": "number"}),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
||||
0
tests/test_filter_pydantic_sub_model/__init__.py
Normal file
0
tests/test_filter_pydantic_sub_model/__init__.py
Normal file
35
tests/test_filter_pydantic_sub_model/app_pv1.py
Normal file
35
tests/test_filter_pydantic_sub_model/app_pv1.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, FastAPI
|
||||
from pydantic import BaseModel, validator
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class ModelB(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
class ModelC(ModelB):
|
||||
password: str
|
||||
|
||||
|
||||
class ModelA(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
model_b: ModelB
|
||||
|
||||
@validator("name")
|
||||
def lower_username(cls, name: str, values):
|
||||
if not name.endswith("A"):
|
||||
raise ValueError("name must end in A")
|
||||
return name
|
||||
|
||||
|
||||
async def get_model_c() -> ModelC:
|
||||
return ModelC(username="test-user", password="test-password")
|
||||
|
||||
|
||||
@app.get("/model/{name}", response_model=ModelA)
|
||||
async def get_model_a(name: str, model_c=Depends(get_model_c)):
|
||||
return {"name": name, "description": "model-a-desc", "model_b": model_c}
|
||||
@@ -1,46 +1,20 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.exceptions import ResponseValidationError
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, ValidationError, validator
|
||||
|
||||
app = FastAPI()
|
||||
from ..utils import needs_pydanticv1
|
||||
|
||||
|
||||
class ModelB(BaseModel):
|
||||
username: str
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from .app_pv1 import app
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
class ModelC(ModelB):
|
||||
password: str
|
||||
|
||||
|
||||
class ModelA(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
model_b: ModelB
|
||||
|
||||
@validator("name")
|
||||
def lower_username(cls, name: str, values):
|
||||
if not name.endswith("A"):
|
||||
raise ValueError("name must end in A")
|
||||
return name
|
||||
|
||||
|
||||
async def get_model_c() -> ModelC:
|
||||
return ModelC(username="test-user", password="test-password")
|
||||
|
||||
|
||||
@app.get("/model/{name}", response_model=ModelA)
|
||||
async def get_model_a(name: str, model_c=Depends(get_model_c)):
|
||||
return {"name": name, "description": "model-a-desc", "model_b": model_c}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_filter_sub_model():
|
||||
@needs_pydanticv1
|
||||
def test_filter_sub_model(client: TestClient):
|
||||
response = client.get("/model/modelA")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
@@ -50,8 +24,9 @@ def test_filter_sub_model():
|
||||
}
|
||||
|
||||
|
||||
def test_validator_is_cloned():
|
||||
with pytest.raises(ValidationError) as err:
|
||||
@needs_pydanticv1
|
||||
def test_validator_is_cloned(client: TestClient):
|
||||
with pytest.raises(ResponseValidationError) as err:
|
||||
client.get("/model/modelX")
|
||||
assert err.value.errors() == [
|
||||
{
|
||||
@@ -62,7 +37,8 @@ def test_validator_is_cloned():
|
||||
]
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
@needs_pydanticv1
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
182
tests/test_filter_pydantic_sub_model_pv2.py
Normal file
182
tests/test_filter_pydantic_sub_model_pv2.py
Normal file
@@ -0,0 +1,182 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.exceptions import ResponseValidationError
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from .utils import needs_pydanticv2
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from pydantic import BaseModel, FieldValidationInfo, field_validator
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
class ModelB(BaseModel):
|
||||
username: str
|
||||
|
||||
class ModelC(ModelB):
|
||||
password: str
|
||||
|
||||
class ModelA(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
foo: ModelB
|
||||
|
||||
@field_validator("name")
|
||||
def lower_username(cls, name: str, info: FieldValidationInfo):
|
||||
if not name.endswith("A"):
|
||||
raise ValueError("name must end in A")
|
||||
return name
|
||||
|
||||
async def get_model_c() -> ModelC:
|
||||
return ModelC(username="test-user", password="test-password")
|
||||
|
||||
@app.get("/model/{name}", response_model=ModelA)
|
||||
async def get_model_a(name: str, model_c=Depends(get_model_c)):
|
||||
return {"name": name, "description": "model-a-desc", "foo": model_c}
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_filter_sub_model(client: TestClient):
|
||||
response = client.get("/model/modelA")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"name": "modelA",
|
||||
"description": "model-a-desc",
|
||||
"foo": {"username": "test-user"},
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_validator_is_cloned(client: TestClient):
|
||||
with pytest.raises(ResponseValidationError) as err:
|
||||
client.get("/model/modelX")
|
||||
assert err.value.errors() == [
|
||||
IsDict(
|
||||
{
|
||||
"type": "value_error",
|
||||
"loc": ("response", "name"),
|
||||
"msg": "Value error, name must end in A",
|
||||
"input": "modelX",
|
||||
"ctx": {"error": "name must end in A"},
|
||||
"url": match_pydantic_error_url("value_error"),
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
{
|
||||
"loc": ("response", "name"),
|
||||
"msg": "name must end in A",
|
||||
"type": "value_error",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/model/{name}": {
|
||||
"get": {
|
||||
"summary": "Get Model A",
|
||||
"operationId": "get_model_a_model__name__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Name", "type": "string"},
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/ModelA"}
|
||||
}
|
||||
},
|
||||
},
|
||||
"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"},
|
||||
}
|
||||
},
|
||||
},
|
||||
"ModelA": {
|
||||
"title": "ModelA",
|
||||
"required": ["name", "foo"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
|
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
IsDict({"title": "Description", "type": "string"}),
|
||||
"foo": {"$ref": "#/components/schemas/ModelB"},
|
||||
},
|
||||
},
|
||||
"ModelB": {
|
||||
"title": "ModelB",
|
||||
"required": ["username"],
|
||||
"type": "object",
|
||||
"properties": {"username": {"title": "Username", "type": "string"}},
|
||||
},
|
||||
"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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@@ -104,35 +105,253 @@ def test_get_users_item():
|
||||
assert response.json() == {"item_id": "item01", "user_id": "abc123"}
|
||||
|
||||
|
||||
def test_schema_1():
|
||||
"""Check that the user_id is a required path parameter under /users"""
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
r = response.json()
|
||||
|
||||
d = {
|
||||
"required": True,
|
||||
"schema": {"title": "User Id", "type": "string"},
|
||||
"name": "user_id",
|
||||
"in": "path",
|
||||
assert response.json() == {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/": {
|
||||
"get": {
|
||||
"summary": "Get Users",
|
||||
"operationId": "get_users_users__get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"/users/{user_id}": {
|
||||
"get": {
|
||||
"summary": "Get User",
|
||||
"operationId": "get_user_users__user_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "User Id", "type": "string"},
|
||||
"name": "user_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/items/": {
|
||||
"get": {
|
||||
"summary": "Get Items",
|
||||
"operationId": "get_items_items__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"name": "user_id",
|
||||
"in": "query",
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "User Id",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "User Id", "type": "string"}
|
||||
),
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/items/{item_id}": {
|
||||
"get": {
|
||||
"summary": "Get Item",
|
||||
"operationId": "get_item_items__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
},
|
||||
{
|
||||
"required": False,
|
||||
"name": "user_id",
|
||||
"in": "query",
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "User Id",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "User Id", "type": "string"}
|
||||
),
|
||||
},
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/users/{user_id}/items/": {
|
||||
"get": {
|
||||
"summary": "Get Items",
|
||||
"operationId": "get_items_users__user_id__items__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"name": "user_id",
|
||||
"in": "path",
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "User Id",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "User Id", "type": "string"}
|
||||
),
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/users/{user_id}/items/{item_id}": {
|
||||
"get": {
|
||||
"summary": "Get Item",
|
||||
"operationId": "get_item_users__user_id__items__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
},
|
||||
{
|
||||
"required": True,
|
||||
"name": "user_id",
|
||||
"in": "path",
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "User Id",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "User Id", "type": "string"}
|
||||
),
|
||||
},
|
||||
],
|
||||
"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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
assert d in r["paths"]["/users/{user_id}"]["get"]["parameters"]
|
||||
assert d in r["paths"]["/users/{user_id}/items/"]["get"]["parameters"]
|
||||
|
||||
|
||||
def test_schema_2():
|
||||
"""Check that the user_id is an optional query parameter under /items"""
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
r = response.json()
|
||||
|
||||
d = {
|
||||
"required": False,
|
||||
"schema": {"title": "User Id", "type": "string"},
|
||||
"name": "user_id",
|
||||
"in": "query",
|
||||
}
|
||||
|
||||
assert d in r["paths"]["/items/{item_id}"]["get"]["parameters"]
|
||||
assert d in r["paths"]["/items/"]["get"]["parameters"]
|
||||
|
||||
@@ -5,7 +5,7 @@ from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
from .utils import needs_pydanticv1, needs_pydanticv2
|
||||
|
||||
|
||||
class MyUuid:
|
||||
@@ -26,40 +26,78 @@ class MyUuid:
|
||||
raise TypeError("vars() argument must have __dict__ attribute")
|
||||
|
||||
|
||||
@app.get("/fast_uuid")
|
||||
def return_fast_uuid():
|
||||
# I don't want to import asyncpg for this test so I made my own UUID
|
||||
# Import asyncpg and uncomment the two lines below for the actual bug
|
||||
@needs_pydanticv2
|
||||
def test_pydanticv2():
|
||||
from pydantic import field_serializer
|
||||
|
||||
# from asyncpg.pgproto import pgproto
|
||||
# asyncpg_uuid = pgproto.UUID("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
|
||||
app = FastAPI()
|
||||
|
||||
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
|
||||
assert isinstance(asyncpg_uuid, uuid.UUID)
|
||||
assert type(asyncpg_uuid) != uuid.UUID
|
||||
with pytest.raises(TypeError):
|
||||
vars(asyncpg_uuid)
|
||||
return {"fast_uuid": asyncpg_uuid}
|
||||
@app.get("/fast_uuid")
|
||||
def return_fast_uuid():
|
||||
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
|
||||
assert isinstance(asyncpg_uuid, uuid.UUID)
|
||||
assert type(asyncpg_uuid) != uuid.UUID
|
||||
with pytest.raises(TypeError):
|
||||
vars(asyncpg_uuid)
|
||||
return {"fast_uuid": asyncpg_uuid}
|
||||
|
||||
class SomeCustomClass(BaseModel):
|
||||
model_config = {"arbitrary_types_allowed": True}
|
||||
|
||||
class SomeCustomClass(BaseModel):
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
json_encoders = {uuid.UUID: str}
|
||||
a_uuid: MyUuid
|
||||
|
||||
a_uuid: MyUuid
|
||||
@field_serializer("a_uuid")
|
||||
def serialize_a_uuid(self, v):
|
||||
return str(v)
|
||||
|
||||
@app.get("/get_custom_class")
|
||||
def return_some_user():
|
||||
# Test that the fix also works for custom pydantic classes
|
||||
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
|
||||
|
||||
@app.get("/get_custom_class")
|
||||
def return_some_user():
|
||||
# Test that the fix also works for custom pydantic classes
|
||||
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
|
||||
client = TestClient(app)
|
||||
|
||||
with client:
|
||||
response_simple = client.get("/fast_uuid")
|
||||
response_pydantic = client.get("/get_custom_class")
|
||||
|
||||
assert response_simple.json() == {
|
||||
"fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51"
|
||||
}
|
||||
|
||||
assert response_pydantic.json() == {
|
||||
"a_uuid": "b8799909-f914-42de-91bc-95c819218d01"
|
||||
}
|
||||
|
||||
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
@needs_pydanticv1
|
||||
def test_pydanticv1():
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/fast_uuid")
|
||||
def return_fast_uuid():
|
||||
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
|
||||
assert isinstance(asyncpg_uuid, uuid.UUID)
|
||||
assert type(asyncpg_uuid) != uuid.UUID
|
||||
with pytest.raises(TypeError):
|
||||
vars(asyncpg_uuid)
|
||||
return {"fast_uuid": asyncpg_uuid}
|
||||
|
||||
class SomeCustomClass(BaseModel):
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
json_encoders = {uuid.UUID: str}
|
||||
|
||||
a_uuid: MyUuid
|
||||
|
||||
@app.get("/get_custom_class")
|
||||
def return_some_user():
|
||||
# Test that the fix also works for custom pydantic classes
|
||||
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_dt():
|
||||
with client:
|
||||
response_simple = client.get("/fast_uuid")
|
||||
response_pydantic = client.get("/get_custom_class")
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from pathlib import PurePath, PurePosixPath, PureWindowsPath
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseModel, Field, ValidationError, create_model
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
|
||||
from .utils import needs_pydanticv1, needs_pydanticv2
|
||||
|
||||
|
||||
class Person:
|
||||
@@ -45,22 +49,6 @@ class Unserializable:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ModelWithCustomEncoder(BaseModel):
|
||||
dt_field: datetime
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda dt: dt.replace(
|
||||
microsecond=0, tzinfo=timezone.utc
|
||||
).isoformat()
|
||||
}
|
||||
|
||||
|
||||
class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
|
||||
class Config:
|
||||
pass
|
||||
|
||||
|
||||
class RoleEnum(Enum):
|
||||
admin = "admin"
|
||||
normal = "normal"
|
||||
@@ -69,8 +57,12 @@ class RoleEnum(Enum):
|
||||
class ModelWithConfig(BaseModel):
|
||||
role: Optional[RoleEnum] = None
|
||||
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"use_enum_values": True}
|
||||
else:
|
||||
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
class ModelWithAlias(BaseModel):
|
||||
@@ -83,23 +75,6 @@ class ModelWithDefault(BaseModel):
|
||||
bla: str = "bla"
|
||||
|
||||
|
||||
class ModelWithRoot(BaseModel):
|
||||
__root__: str
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
name="model_with_path", params=[PurePath, PurePosixPath, PureWindowsPath]
|
||||
)
|
||||
def fixture_model_with_path(request):
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
ModelWithPath = create_model(
|
||||
"ModelWithPath", path=(request.param, ...), __config__=Config # type: ignore
|
||||
)
|
||||
return ModelWithPath(path=request.param("/foo", "bar"))
|
||||
|
||||
|
||||
def test_encode_dict():
|
||||
pet = {"name": "Firulais", "owner": {"name": "Foo"}}
|
||||
assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}}
|
||||
@@ -153,14 +128,47 @@ def test_encode_unsupported():
|
||||
jsonable_encoder(unserializable)
|
||||
|
||||
|
||||
def test_encode_custom_json_encoders_model():
|
||||
@needs_pydanticv2
|
||||
def test_encode_custom_json_encoders_model_pydanticv2():
|
||||
from pydantic import field_serializer
|
||||
|
||||
class ModelWithCustomEncoder(BaseModel):
|
||||
dt_field: datetime
|
||||
|
||||
@field_serializer("dt_field")
|
||||
def serialize_dt_field(self, dt):
|
||||
return dt.replace(microsecond=0, tzinfo=timezone.utc).isoformat()
|
||||
|
||||
class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
|
||||
pass
|
||||
|
||||
model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8))
|
||||
assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
|
||||
subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
|
||||
assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
|
||||
|
||||
|
||||
def test_encode_custom_json_encoders_model_subclass():
|
||||
model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
@needs_pydanticv1
|
||||
def test_encode_custom_json_encoders_model_pydanticv1():
|
||||
class ModelWithCustomEncoder(BaseModel):
|
||||
dt_field: datetime
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda dt: dt.replace(
|
||||
microsecond=0, tzinfo=timezone.utc
|
||||
).isoformat()
|
||||
}
|
||||
|
||||
class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
|
||||
class Config:
|
||||
pass
|
||||
|
||||
model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8))
|
||||
assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
|
||||
subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
|
||||
assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
|
||||
|
||||
|
||||
def test_encode_model_with_config():
|
||||
@@ -196,6 +204,7 @@ def test_encode_model_with_default():
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv1
|
||||
def test_custom_encoders():
|
||||
class safe_datetime(datetime):
|
||||
pass
|
||||
@@ -226,14 +235,67 @@ def test_custom_enum_encoders():
|
||||
assert encoded_instance == custom_enum_encoder(instance)
|
||||
|
||||
|
||||
def test_encode_model_with_path(model_with_path):
|
||||
if isinstance(model_with_path.path, PureWindowsPath):
|
||||
expected = "\\foo\\bar"
|
||||
else:
|
||||
expected = "/foo/bar"
|
||||
assert jsonable_encoder(model_with_path) == {"path": expected}
|
||||
def test_encode_model_with_pure_path():
|
||||
class ModelWithPath(BaseModel):
|
||||
path: PurePath
|
||||
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"arbitrary_types_allowed": True}
|
||||
else:
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
obj = ModelWithPath(path=PurePath("/foo", "bar"))
|
||||
assert jsonable_encoder(obj) == {"path": "/foo/bar"}
|
||||
|
||||
|
||||
def test_encode_model_with_pure_posix_path():
|
||||
class ModelWithPath(BaseModel):
|
||||
path: PurePosixPath
|
||||
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"arbitrary_types_allowed": True}
|
||||
else:
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
obj = ModelWithPath(path=PurePosixPath("/foo", "bar"))
|
||||
assert jsonable_encoder(obj) == {"path": "/foo/bar"}
|
||||
|
||||
|
||||
def test_encode_model_with_pure_windows_path():
|
||||
class ModelWithPath(BaseModel):
|
||||
path: PureWindowsPath
|
||||
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"arbitrary_types_allowed": True}
|
||||
else:
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
obj = ModelWithPath(path=PureWindowsPath("/foo", "bar"))
|
||||
assert jsonable_encoder(obj) == {"path": "\\foo\\bar"}
|
||||
|
||||
|
||||
@needs_pydanticv1
|
||||
def test_encode_root():
|
||||
class ModelWithRoot(BaseModel):
|
||||
__root__: str
|
||||
|
||||
model = ModelWithRoot(__root__="Foo")
|
||||
assert jsonable_encoder(model) == "Foo"
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_decimal_encoder_float():
|
||||
data = {"value": Decimal(1.23)}
|
||||
assert jsonable_encoder(data) == {"value": 1.23}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_decimal_encoder_int():
|
||||
data = {"value": Decimal(2)}
|
||||
assert jsonable_encoder(data) == {"value": 2}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from decimal import Decimal
|
||||
from typing import List
|
||||
|
||||
from dirty_equals import IsDict, IsOneOf
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
from pydantic import BaseModel, condecimal
|
||||
|
||||
app = FastAPI()
|
||||
@@ -21,59 +23,115 @@ def save_item_no_body(item: List[Item]):
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
single_error = {
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"limit_value": 0.0},
|
||||
"loc": ["body", 0, "age"],
|
||||
"msg": "ensure this value is greater than 0",
|
||||
"type": "value_error.number.not_gt",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
multiple_errors = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", 0, "name"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", 0, "age"],
|
||||
"msg": "value is not a valid decimal",
|
||||
"type": "type_error.decimal",
|
||||
},
|
||||
{
|
||||
"loc": ["body", 1, "name"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", 1, "age"],
|
||||
"msg": "value is not a valid decimal",
|
||||
"type": "type_error.decimal",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_put_correct_body():
|
||||
response = client.post("/items/", json=[{"name": "Foo", "age": 5}])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"item": [{"name": "Foo", "age": 5}]}
|
||||
assert response.json() == {
|
||||
"item": [
|
||||
{
|
||||
"name": "Foo",
|
||||
"age": IsOneOf(
|
||||
5,
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"5",
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_jsonable_encoder_requiring_error():
|
||||
response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}])
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == single_error
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "greater_than",
|
||||
"loc": ["body", 0, "age"],
|
||||
"msg": "Input should be greater than 0",
|
||||
"input": -1.0,
|
||||
"ctx": {"gt": 0.0},
|
||||
"url": match_pydantic_error_url("greater_than"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"limit_value": 0.0},
|
||||
"loc": ["body", 0, "age"],
|
||||
"msg": "ensure this value is greater than 0",
|
||||
"type": "value_error.number.not_gt",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_put_incorrect_body_multiple():
|
||||
response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}])
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == multiple_errors
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", 0, "name"],
|
||||
"msg": "Field required",
|
||||
"input": {"age": "five"},
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "decimal_parsing",
|
||||
"loc": ["body", 0, "age"],
|
||||
"msg": "Input should be a valid decimal",
|
||||
"input": "five",
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", 1, "name"],
|
||||
"msg": "Field required",
|
||||
"input": {"age": "six"},
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "decimal_parsing",
|
||||
"loc": ["body", 1, "age"],
|
||||
"msg": "Input should be a valid decimal",
|
||||
"input": "six",
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", 0, "name"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", 0, "age"],
|
||||
"msg": "value is not a valid decimal",
|
||||
"type": "type_error.decimal",
|
||||
},
|
||||
{
|
||||
"loc": ["body", 1, "name"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", 1, "age"],
|
||||
"msg": "value is not a valid decimal",
|
||||
"type": "type_error.decimal",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
@@ -126,11 +184,23 @@ def test_openapi_schema():
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"age": {
|
||||
"title": "Age",
|
||||
"exclusiveMinimum": 0.0,
|
||||
"type": "number",
|
||||
},
|
||||
"age": IsDict(
|
||||
{
|
||||
"title": "Age",
|
||||
"anyOf": [
|
||||
{"exclusiveMinimum": 0.0, "type": "number"},
|
||||
{"type": "string"},
|
||||
],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "Age",
|
||||
"exclusiveMinimum": 0.0,
|
||||
"type": "number",
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from typing import List
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@@ -14,22 +16,6 @@ def read_items(q: List[int] = Query(default=None)):
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
multiple_errors = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q", 0],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
},
|
||||
{
|
||||
"loc": ["query", "q", 1],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_multi_query():
|
||||
response = client.get("/items/?q=5&q=6")
|
||||
assert response.status_code == 200, response.text
|
||||
@@ -39,7 +25,42 @@ def test_multi_query():
|
||||
def test_multi_query_incorrect():
|
||||
response = client.get("/items/?q=five&q=six")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == multiple_errors
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["query", "q", 0],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "five",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
},
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["query", "q", 1],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "six",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q", 0],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
},
|
||||
{
|
||||
"loc": ["query", "q", 1],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@@ -52,11 +53,21 @@ def test_openapi():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"title": "Standard Query Param",
|
||||
"type": "integer",
|
||||
"default": 50,
|
||||
},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"default": 50,
|
||||
"title": "Standard Query Param",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "Standard Query Param",
|
||||
"type": "integer",
|
||||
"default": 50,
|
||||
}
|
||||
),
|
||||
"name": "standard_query_param",
|
||||
"in": "query",
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from dirty_equals import IsOneOf
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@@ -35,10 +36,20 @@ def test_openapi_schema():
|
||||
"servers": [
|
||||
{"url": "/", "description": "Default, relative server"},
|
||||
{
|
||||
"url": "http://staging.localhost.tiangolo.com:8000",
|
||||
"url": IsOneOf(
|
||||
"http://staging.localhost.tiangolo.com:8000/",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"http://staging.localhost.tiangolo.com:8000",
|
||||
),
|
||||
"description": "Staging but actually localhost still",
|
||||
},
|
||||
{"url": "https://prod.example.com"},
|
||||
{
|
||||
"url": IsOneOf(
|
||||
"https://prod.example.com/",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"https://prod.example.com",
|
||||
)
|
||||
},
|
||||
],
|
||||
"paths": {
|
||||
"/foo": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Any, List
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsOneOf
|
||||
from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query
|
||||
|
||||
test_data: List[Any] = ["teststr", None, ..., 1, []]
|
||||
@@ -10,34 +10,137 @@ def get_user():
|
||||
return {} # pragma: no cover
|
||||
|
||||
|
||||
@pytest.fixture(scope="function", params=test_data)
|
||||
def params(request):
|
||||
return request.param
|
||||
def test_param_repr_str():
|
||||
assert repr(Param("teststr")) == "Param(teststr)"
|
||||
|
||||
|
||||
def test_param_repr(params):
|
||||
assert repr(Param(params)) == "Param(" + str(params) + ")"
|
||||
def test_param_repr_none():
|
||||
assert repr(Param(None)) == "Param(None)"
|
||||
|
||||
|
||||
def test_param_repr_ellipsis():
|
||||
assert repr(Param(...)) == IsOneOf(
|
||||
"Param(PydanticUndefined)",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"Param(Ellipsis)",
|
||||
)
|
||||
|
||||
|
||||
def test_param_repr_number():
|
||||
assert repr(Param(1)) == "Param(1)"
|
||||
|
||||
|
||||
def test_param_repr_list():
|
||||
assert repr(Param([])) == "Param([])"
|
||||
|
||||
|
||||
def test_path_repr():
|
||||
assert repr(Path()) == "Path(Ellipsis)"
|
||||
assert repr(Path(...)) == "Path(Ellipsis)"
|
||||
assert repr(Path()) == IsOneOf(
|
||||
"Path(PydanticUndefined)",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"Path(Ellipsis)",
|
||||
)
|
||||
assert repr(Path(...)) == IsOneOf(
|
||||
"Path(PydanticUndefined)",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"Path(Ellipsis)",
|
||||
)
|
||||
|
||||
|
||||
def test_query_repr(params):
|
||||
assert repr(Query(params)) == "Query(" + str(params) + ")"
|
||||
def test_query_repr_str():
|
||||
assert repr(Query("teststr")) == "Query(teststr)"
|
||||
|
||||
|
||||
def test_header_repr(params):
|
||||
assert repr(Header(params)) == "Header(" + str(params) + ")"
|
||||
def test_query_repr_none():
|
||||
assert repr(Query(None)) == "Query(None)"
|
||||
|
||||
|
||||
def test_cookie_repr(params):
|
||||
assert repr(Cookie(params)) == "Cookie(" + str(params) + ")"
|
||||
def test_query_repr_ellipsis():
|
||||
assert repr(Query(...)) == IsOneOf(
|
||||
"Query(PydanticUndefined)",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"Query(Ellipsis)",
|
||||
)
|
||||
|
||||
|
||||
def test_body_repr(params):
|
||||
assert repr(Body(params)) == "Body(" + str(params) + ")"
|
||||
def test_query_repr_number():
|
||||
assert repr(Query(1)) == "Query(1)"
|
||||
|
||||
|
||||
def test_query_repr_list():
|
||||
assert repr(Query([])) == "Query([])"
|
||||
|
||||
|
||||
def test_header_repr_str():
|
||||
assert repr(Header("teststr")) == "Header(teststr)"
|
||||
|
||||
|
||||
def test_header_repr_none():
|
||||
assert repr(Header(None)) == "Header(None)"
|
||||
|
||||
|
||||
def test_header_repr_ellipsis():
|
||||
assert repr(Header(...)) == IsOneOf(
|
||||
"Header(PydanticUndefined)",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"Header(Ellipsis)",
|
||||
)
|
||||
|
||||
|
||||
def test_header_repr_number():
|
||||
assert repr(Header(1)) == "Header(1)"
|
||||
|
||||
|
||||
def test_header_repr_list():
|
||||
assert repr(Header([])) == "Header([])"
|
||||
|
||||
|
||||
def test_cookie_repr_str():
|
||||
assert repr(Cookie("teststr")) == "Cookie(teststr)"
|
||||
|
||||
|
||||
def test_cookie_repr_none():
|
||||
assert repr(Cookie(None)) == "Cookie(None)"
|
||||
|
||||
|
||||
def test_cookie_repr_ellipsis():
|
||||
assert repr(Cookie(...)) == IsOneOf(
|
||||
"Cookie(PydanticUndefined)",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"Cookie(Ellipsis)",
|
||||
)
|
||||
|
||||
|
||||
def test_cookie_repr_number():
|
||||
assert repr(Cookie(1)) == "Cookie(1)"
|
||||
|
||||
|
||||
def test_cookie_repr_list():
|
||||
assert repr(Cookie([])) == "Cookie([])"
|
||||
|
||||
|
||||
def test_body_repr_str():
|
||||
assert repr(Body("teststr")) == "Body(teststr)"
|
||||
|
||||
|
||||
def test_body_repr_none():
|
||||
assert repr(Body(None)) == "Body(None)"
|
||||
|
||||
|
||||
def test_body_repr_ellipsis():
|
||||
assert repr(Body(...)) == IsOneOf(
|
||||
"Body(PydanticUndefined)",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"Body(Ellipsis)",
|
||||
)
|
||||
|
||||
|
||||
def test_body_repr_number():
|
||||
assert repr(Body(1)) == "Body(1)"
|
||||
|
||||
|
||||
def test_body_repr_list():
|
||||
assert repr(Body([])) == "Body([])"
|
||||
|
||||
|
||||
def test_depends_repr():
|
||||
|
||||
1413
tests/test_path.py
1413
tests/test_path.py
File diff suppressed because it is too large
Load Diff
@@ -1,62 +1,410 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from .main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response_missing = {
|
||||
"detail": [
|
||||
|
||||
def test_query():
|
||||
response = client.get("/query")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response_not_valid_int = {
|
||||
"detail": [
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected_status,expected_response",
|
||||
[
|
||||
("/query", 422, response_missing),
|
||||
("/query?query=baz", 200, "foo bar baz"),
|
||||
("/query?not_declared=baz", 422, response_missing),
|
||||
("/query/optional", 200, "foo bar"),
|
||||
("/query/optional?query=baz", 200, "foo bar baz"),
|
||||
("/query/optional?not_declared=baz", 200, "foo bar"),
|
||||
("/query/int", 422, response_missing),
|
||||
("/query/int?query=42", 200, "foo bar 42"),
|
||||
("/query/int?query=42.5", 422, response_not_valid_int),
|
||||
("/query/int?query=baz", 422, response_not_valid_int),
|
||||
("/query/int?not_declared=baz", 422, response_missing),
|
||||
("/query/int/optional", 200, "foo bar"),
|
||||
("/query/int/optional?query=50", 200, "foo bar 50"),
|
||||
("/query/int/optional?query=foo", 422, response_not_valid_int),
|
||||
("/query/int/default", 200, "foo bar 10"),
|
||||
("/query/int/default?query=50", 200, "foo bar 50"),
|
||||
("/query/int/default?query=foo", 422, response_not_valid_int),
|
||||
("/query/param", 200, "foo bar"),
|
||||
("/query/param?query=50", 200, "foo bar 50"),
|
||||
("/query/param-required", 422, response_missing),
|
||||
("/query/param-required?query=50", 200, "foo bar 50"),
|
||||
("/query/param-required/int", 422, response_missing),
|
||||
("/query/param-required/int?query=50", 200, "foo bar 50"),
|
||||
("/query/param-required/int?query=foo", 422, response_not_valid_int),
|
||||
("/query/frozenset/?query=1&query=1&query=2", 200, "1,2"),
|
||||
],
|
||||
)
|
||||
def test_get_path(path, expected_status, expected_response):
|
||||
response = client.get(path)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
def test_query_query_baz():
|
||||
response = client.get("/query?query=baz")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar baz"
|
||||
|
||||
|
||||
def test_query_not_declared_baz():
|
||||
response = client.get("/query?not_declared=baz")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_optional():
|
||||
response = client.get("/query/optional")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar"
|
||||
|
||||
|
||||
def test_query_optional_query_baz():
|
||||
response = client.get("/query/optional?query=baz")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar baz"
|
||||
|
||||
|
||||
def test_query_optional_not_declared_baz():
|
||||
response = client.get("/query/optional?not_declared=baz")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar"
|
||||
|
||||
|
||||
def test_query_int():
|
||||
response = client.get("/query/int")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_int_query_42():
|
||||
response = client.get("/query/int?query=42")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar 42"
|
||||
|
||||
|
||||
def test_query_int_query_42_5():
|
||||
response = client.get("/query/int?query=42.5")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "42.5",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_int_query_baz():
|
||||
response = client.get("/query/int?query=baz")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "baz",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_int_not_declared_baz():
|
||||
response = client.get("/query/int?not_declared=baz")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_int_optional():
|
||||
response = client.get("/query/int/optional")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar"
|
||||
|
||||
|
||||
def test_query_int_optional_query_50():
|
||||
response = client.get("/query/int/optional?query=50")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar 50"
|
||||
|
||||
|
||||
def test_query_int_optional_query_foo():
|
||||
response = client.get("/query/int/optional?query=foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "foo",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_int_default():
|
||||
response = client.get("/query/int/default")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar 10"
|
||||
|
||||
|
||||
def test_query_int_default_query_50():
|
||||
response = client.get("/query/int/default?query=50")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar 50"
|
||||
|
||||
|
||||
def test_query_int_default_query_foo():
|
||||
response = client.get("/query/int/default?query=foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "foo",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_param():
|
||||
response = client.get("/query/param")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar"
|
||||
|
||||
|
||||
def test_query_param_query_50():
|
||||
response = client.get("/query/param?query=50")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar 50"
|
||||
|
||||
|
||||
def test_query_param_required():
|
||||
response = client.get("/query/param-required")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_param_required_query_50():
|
||||
response = client.get("/query/param-required?query=50")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar 50"
|
||||
|
||||
|
||||
def test_query_param_required_int():
|
||||
response = client.get("/query/param-required/int")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_param_required_int_query_50():
|
||||
response = client.get("/query/param-required/int?query=50")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "foo bar 50"
|
||||
|
||||
|
||||
def test_query_param_required_int_query_foo():
|
||||
response = client.get("/query/param-required/int?query=foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["query", "query"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "foo",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "query"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_query_frozenset_query_1_query_1_query_2():
|
||||
response = client.get("/query/frozenset/?query=1&query=1&query=2")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "1,2"
|
||||
|
||||
@@ -2,48 +2,83 @@ from typing import Any
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PersonBase(BaseModel):
|
||||
name: str
|
||||
lastname: str
|
||||
|
||||
|
||||
class Person(PersonBase):
|
||||
@property
|
||||
def full_name(self) -> str:
|
||||
return f"{self.name} {self.lastname}"
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
read_with_orm_mode = True
|
||||
|
||||
|
||||
class PersonCreate(PersonBase):
|
||||
pass
|
||||
|
||||
|
||||
class PersonRead(PersonBase):
|
||||
full_name: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/people/", response_model=PersonRead)
|
||||
def create_person(person: PersonCreate) -> Any:
|
||||
db_person = Person.from_orm(person)
|
||||
return db_person
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from .utils import needs_pydanticv1, needs_pydanticv2
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_read_with_orm_mode() -> None:
|
||||
class PersonBase(BaseModel):
|
||||
name: str
|
||||
lastname: str
|
||||
|
||||
class Person(PersonBase):
|
||||
@property
|
||||
def full_name(self) -> str:
|
||||
return f"{self.name} {self.lastname}"
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class PersonCreate(PersonBase):
|
||||
pass
|
||||
|
||||
class PersonRead(PersonBase):
|
||||
full_name: str
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.post("/people/", response_model=PersonRead)
|
||||
def create_person(person: PersonCreate) -> Any:
|
||||
db_person = Person.model_validate(person)
|
||||
return db_person
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
person_data = {"name": "Dive", "lastname": "Wilson"}
|
||||
response = client.post("/people/", json=person_data)
|
||||
data = response.json()
|
||||
assert response.status_code == 200, response.text
|
||||
assert data["name"] == person_data["name"]
|
||||
assert data["lastname"] == person_data["lastname"]
|
||||
assert data["full_name"] == person_data["name"] + " " + person_data["lastname"]
|
||||
|
||||
|
||||
@needs_pydanticv1
|
||||
def test_read_with_orm_mode_pv1() -> None:
|
||||
class PersonBase(BaseModel):
|
||||
name: str
|
||||
lastname: str
|
||||
|
||||
class Person(PersonBase):
|
||||
@property
|
||||
def full_name(self) -> str:
|
||||
return f"{self.name} {self.lastname}"
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
read_with_orm_mode = True
|
||||
|
||||
class PersonCreate(PersonBase):
|
||||
pass
|
||||
|
||||
class PersonRead(PersonBase):
|
||||
full_name: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.post("/people/", response_model=PersonRead)
|
||||
def create_person(person: PersonCreate) -> Any:
|
||||
db_person = Person.from_orm(person)
|
||||
return db_person
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
person_data = {"name": "Dive", "lastname": "Wilson"}
|
||||
response = client.post("/people/", json=person_data)
|
||||
data = response.json()
|
||||
|
||||
@@ -39,7 +39,6 @@ client = TestClient(app)
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
# insert_assert(response.json())
|
||||
assert response.json() == {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@@ -14,13 +15,24 @@ class Model(BaseModel):
|
||||
class ModelNoAlias(BaseModel):
|
||||
name: str
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"description": (
|
||||
"response_model_by_alias=False is basically a quick hack, to support "
|
||||
"proper OpenAPI use another model with the correct field names"
|
||||
)
|
||||
}
|
||||
if PYDANTIC_V2:
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"description": (
|
||||
"response_model_by_alias=False is basically a quick hack, to support "
|
||||
"proper OpenAPI use another model with the correct field names"
|
||||
)
|
||||
}
|
||||
)
|
||||
else:
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"description": (
|
||||
"response_model_by_alias=False is basically a quick hack, to support "
|
||||
"proper OpenAPI use another model with the correct field names"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@app.get("/dict", response_model=Model, response_model_by_alias=False)
|
||||
|
||||
@@ -2,10 +2,10 @@ from typing import List, Union
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.exceptions import FastAPIError
|
||||
from fastapi.exceptions import FastAPIError, ResponseValidationError
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class BaseUser(BaseModel):
|
||||
@@ -277,12 +277,12 @@ def test_response_model_no_annotation_return_exact_dict():
|
||||
|
||||
|
||||
def test_response_model_no_annotation_return_invalid_dict():
|
||||
with pytest.raises(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
client.get("/response_model-no_annotation-return_invalid_dict")
|
||||
|
||||
|
||||
def test_response_model_no_annotation_return_invalid_model():
|
||||
with pytest.raises(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
client.get("/response_model-no_annotation-return_invalid_model")
|
||||
|
||||
|
||||
@@ -313,12 +313,12 @@ def test_no_response_model_annotation_return_exact_dict():
|
||||
|
||||
|
||||
def test_no_response_model_annotation_return_invalid_dict():
|
||||
with pytest.raises(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
client.get("/no_response_model-annotation-return_invalid_dict")
|
||||
|
||||
|
||||
def test_no_response_model_annotation_return_invalid_model():
|
||||
with pytest.raises(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
client.get("/no_response_model-annotation-return_invalid_model")
|
||||
|
||||
|
||||
@@ -395,12 +395,12 @@ def test_response_model_model1_annotation_model2_return_exact_dict():
|
||||
|
||||
|
||||
def test_response_model_model1_annotation_model2_return_invalid_dict():
|
||||
with pytest.raises(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
client.get("/response_model_model1-annotation_model2-return_invalid_dict")
|
||||
|
||||
|
||||
def test_response_model_model1_annotation_model2_return_invalid_model():
|
||||
with pytest.raises(ValidationError):
|
||||
with pytest.raises(ResponseValidationError):
|
||||
client.get("/response_model_model1-annotation_model2-return_invalid_model")
|
||||
|
||||
|
||||
|
||||
81
tests/test_response_model_data_filter.py
Normal file
81
tests/test_response_model_data_filter.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
email: str
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str
|
||||
|
||||
|
||||
class UserDB(UserBase):
|
||||
hashed_password: str
|
||||
|
||||
|
||||
class PetDB(BaseModel):
|
||||
name: str
|
||||
owner: UserDB
|
||||
|
||||
|
||||
class PetOut(BaseModel):
|
||||
name: str
|
||||
owner: UserBase
|
||||
|
||||
|
||||
@app.post("/users/", response_model=UserBase)
|
||||
async def create_user(user: UserCreate):
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/pets/{pet_id}", response_model=PetOut)
|
||||
async def read_pet(pet_id: int):
|
||||
user = UserDB(
|
||||
email="johndoe@example.com",
|
||||
hashed_password="secrethashed",
|
||||
)
|
||||
pet = PetDB(name="Nibbler", owner=user)
|
||||
return pet
|
||||
|
||||
|
||||
@app.get("/pets/", response_model=List[PetOut])
|
||||
async def read_pets():
|
||||
user = UserDB(
|
||||
email="johndoe@example.com",
|
||||
hashed_password="secrethashed",
|
||||
)
|
||||
pet1 = PetDB(name="Nibbler", owner=user)
|
||||
pet2 = PetDB(name="Zoidberg", owner=user)
|
||||
return [pet1, pet2]
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_filter_top_level_model():
|
||||
response = client.post(
|
||||
"/users", json={"email": "johndoe@example.com", "password": "secret"}
|
||||
)
|
||||
assert response.json() == {"email": "johndoe@example.com"}
|
||||
|
||||
|
||||
def test_filter_second_level_model():
|
||||
response = client.get("/pets/1")
|
||||
assert response.json() == {
|
||||
"name": "Nibbler",
|
||||
"owner": {"email": "johndoe@example.com"},
|
||||
}
|
||||
|
||||
|
||||
def test_list_of_models():
|
||||
response = client.get("/pets/")
|
||||
assert response.json() == [
|
||||
{"name": "Nibbler", "owner": {"email": "johndoe@example.com"}},
|
||||
{"name": "Zoidberg", "owner": {"email": "johndoe@example.com"}},
|
||||
]
|
||||
83
tests/test_response_model_data_filter_no_inheritance.py
Normal file
83
tests/test_response_model_data_filter_no_inheritance.py
Normal file
@@ -0,0 +1,83 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
email: str
|
||||
password: str
|
||||
|
||||
|
||||
class UserDB(BaseModel):
|
||||
email: str
|
||||
hashed_password: str
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
email: str
|
||||
|
||||
|
||||
class PetDB(BaseModel):
|
||||
name: str
|
||||
owner: UserDB
|
||||
|
||||
|
||||
class PetOut(BaseModel):
|
||||
name: str
|
||||
owner: User
|
||||
|
||||
|
||||
@app.post("/users/", response_model=User)
|
||||
async def create_user(user: UserCreate):
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/pets/{pet_id}", response_model=PetOut)
|
||||
async def read_pet(pet_id: int):
|
||||
user = UserDB(
|
||||
email="johndoe@example.com",
|
||||
hashed_password="secrethashed",
|
||||
)
|
||||
pet = PetDB(name="Nibbler", owner=user)
|
||||
return pet
|
||||
|
||||
|
||||
@app.get("/pets/", response_model=List[PetOut])
|
||||
async def read_pets():
|
||||
user = UserDB(
|
||||
email="johndoe@example.com",
|
||||
hashed_password="secrethashed",
|
||||
)
|
||||
pet1 = PetDB(name="Nibbler", owner=user)
|
||||
pet2 = PetDB(name="Zoidberg", owner=user)
|
||||
return [pet1, pet2]
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_filter_top_level_model():
|
||||
response = client.post(
|
||||
"/users", json={"email": "johndoe@example.com", "password": "secret"}
|
||||
)
|
||||
assert response.json() == {"email": "johndoe@example.com"}
|
||||
|
||||
|
||||
def test_filter_second_level_model():
|
||||
response = client.get("/pets/1")
|
||||
assert response.json() == {
|
||||
"name": "Nibbler",
|
||||
"owner": {"email": "johndoe@example.com"},
|
||||
}
|
||||
|
||||
|
||||
def test_list_of_models():
|
||||
response = client.get("/pets/")
|
||||
assert response.json() == [
|
||||
{"name": "Nibbler", "owner": {"email": "johndoe@example.com"}},
|
||||
{"name": "Zoidberg", "owner": {"email": "johndoe@example.com"}},
|
||||
]
|
||||
@@ -1,8 +1,10 @@
|
||||
from typing import Union
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Body, Cookie, FastAPI, Header, Path, Query
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@@ -10,8 +12,14 @@ app = FastAPI()
|
||||
class Item(BaseModel):
|
||||
data: str
|
||||
|
||||
class Config:
|
||||
schema_extra = {"example": {"data": "Data in schema_extra"}}
|
||||
if PYDANTIC_V2:
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={"example": {"data": "Data in schema_extra"}}
|
||||
)
|
||||
else:
|
||||
|
||||
class Config:
|
||||
schema_extra = {"example": {"data": "Data in schema_extra"}}
|
||||
|
||||
|
||||
@app.post("/schema_extra/")
|
||||
@@ -537,7 +545,16 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Data", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{"title": "Data", "type": "string"}
|
||||
),
|
||||
"example": "query1",
|
||||
"name": "data",
|
||||
"in": "query",
|
||||
@@ -568,7 +585,16 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Data", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{"title": "Data", "type": "string"}
|
||||
),
|
||||
"examples": {
|
||||
"example1": {
|
||||
"summary": "Query example 1",
|
||||
@@ -605,7 +631,16 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Data", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{"title": "Data", "type": "string"}
|
||||
),
|
||||
"examples": {
|
||||
"example1": {
|
||||
"summary": "Query example 1",
|
||||
@@ -642,7 +677,16 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Data", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{"title": "Data", "type": "string"}
|
||||
),
|
||||
"example": "header1",
|
||||
"name": "data",
|
||||
"in": "header",
|
||||
@@ -673,7 +717,16 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Data", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{"title": "Data", "type": "string"}
|
||||
),
|
||||
"examples": {
|
||||
"example1": {
|
||||
"summary": "header example 1",
|
||||
@@ -710,7 +763,16 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Data", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{"title": "Data", "type": "string"}
|
||||
),
|
||||
"examples": {
|
||||
"example1": {
|
||||
"summary": "Query example 1",
|
||||
@@ -747,7 +809,16 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Data", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{"title": "Data", "type": "string"}
|
||||
),
|
||||
"example": "cookie1",
|
||||
"name": "data",
|
||||
"in": "cookie",
|
||||
@@ -778,7 +849,16 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Data", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{"title": "Data", "type": "string"}
|
||||
),
|
||||
"examples": {
|
||||
"example1": {
|
||||
"summary": "cookie example 1",
|
||||
@@ -815,7 +895,16 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Data", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Data",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: Remove this when deprecating Pydantic v1
|
||||
{"title": "Data", "type": "string"}
|
||||
),
|
||||
"examples": {
|
||||
"example1": {
|
||||
"summary": "Query example 1",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
@@ -59,76 +60,136 @@ def test_security_oauth2_password_bearer_no_header():
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
required_params = {
|
||||
"detail": [
|
||||
def test_strict_login_no_data():
|
||||
response = client.post("/login")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "username"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "password"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
grant_type_required = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "username"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "password"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
grant_type_incorrect = {
|
||||
"detail": [
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": 'string does not match regex "password"',
|
||||
"type": "value_error.str.regex",
|
||||
"ctx": {"pattern": "password"},
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "username"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "password"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data,expected_status,expected_response",
|
||||
[
|
||||
(None, 422, required_params),
|
||||
({"username": "johndoe", "password": "secret"}, 422, grant_type_required),
|
||||
(
|
||||
{"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
|
||||
422,
|
||||
grant_type_incorrect,
|
||||
),
|
||||
(
|
||||
{"username": "johndoe", "password": "secret", "grant_type": "password"},
|
||||
200,
|
||||
{
|
||||
"grant_type": "password",
|
||||
"username": "johndoe",
|
||||
"password": "secret",
|
||||
"scopes": [],
|
||||
"client_id": None,
|
||||
"client_secret": None,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_strict_login(data, expected_status, expected_response):
|
||||
response = client.post("/login", data=data)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
def test_strict_login_no_grant_type():
|
||||
response = client.post("/login", data={"username": "johndoe", "password": "secret"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_strict_login_incorrect_grant_type():
|
||||
response = client.post(
|
||||
"/login",
|
||||
data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "string_pattern_mismatch",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "String should match pattern 'password'",
|
||||
"input": "incorrect",
|
||||
"ctx": {"pattern": "password"},
|
||||
"url": match_pydantic_error_url("string_pattern_mismatch"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": 'string does not match regex "password"',
|
||||
"type": "value_error.str.regex",
|
||||
"ctx": {"pattern": "password"},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_strict_login_correct_grant_type():
|
||||
response = client.post(
|
||||
"/login",
|
||||
data={"username": "johndoe", "password": "secret", "grant_type": "password"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"grant_type": "password",
|
||||
"username": "johndoe",
|
||||
"password": "secret",
|
||||
"scopes": [],
|
||||
"client_id": None,
|
||||
"client_secret": None,
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
@@ -199,8 +260,26 @@ def test_openapi_schema():
|
||||
"username": {"title": "Username", "type": "string"},
|
||||
"password": {"title": "Password", "type": "string"},
|
||||
"scope": {"title": "Scope", "type": "string", "default": ""},
|
||||
"client_id": {"title": "Client Id", "type": "string"},
|
||||
"client_secret": {"title": "Client Secret", "type": "string"},
|
||||
"client_id": IsDict(
|
||||
{
|
||||
"title": "Client Id",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Client Id", "type": "string"}
|
||||
),
|
||||
"client_secret": IsDict(
|
||||
{
|
||||
"title": "Client Secret",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Client Secret", "type": "string"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
@@ -63,76 +64,136 @@ def test_security_oauth2_password_bearer_no_header():
|
||||
assert response.json() == {"msg": "Create an account first"}
|
||||
|
||||
|
||||
required_params = {
|
||||
"detail": [
|
||||
def test_strict_login_no_data():
|
||||
response = client.post("/login")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "username"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "password"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
grant_type_required = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "username"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "password"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
grant_type_incorrect = {
|
||||
"detail": [
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": 'string does not match regex "password"',
|
||||
"type": "value_error.str.regex",
|
||||
"ctx": {"pattern": "password"},
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "username"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "password"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data,expected_status,expected_response",
|
||||
[
|
||||
(None, 422, required_params),
|
||||
({"username": "johndoe", "password": "secret"}, 422, grant_type_required),
|
||||
(
|
||||
{"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
|
||||
422,
|
||||
grant_type_incorrect,
|
||||
),
|
||||
(
|
||||
{"username": "johndoe", "password": "secret", "grant_type": "password"},
|
||||
200,
|
||||
{
|
||||
"grant_type": "password",
|
||||
"username": "johndoe",
|
||||
"password": "secret",
|
||||
"scopes": [],
|
||||
"client_id": None,
|
||||
"client_secret": None,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_strict_login(data, expected_status, expected_response):
|
||||
response = client.post("/login", data=data)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
def test_strict_login_no_grant_type():
|
||||
response = client.post("/login", data={"username": "johndoe", "password": "secret"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_strict_login_incorrect_grant_type():
|
||||
response = client.post(
|
||||
"/login",
|
||||
data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "string_pattern_mismatch",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "String should match pattern 'password'",
|
||||
"input": "incorrect",
|
||||
"ctx": {"pattern": "password"},
|
||||
"url": match_pydantic_error_url("string_pattern_mismatch"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": 'string does not match regex "password"',
|
||||
"type": "value_error.str.regex",
|
||||
"ctx": {"pattern": "password"},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_strict_login_correct_data():
|
||||
response = client.post(
|
||||
"/login",
|
||||
data={"username": "johndoe", "password": "secret", "grant_type": "password"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"grant_type": "password",
|
||||
"username": "johndoe",
|
||||
"password": "secret",
|
||||
"scopes": [],
|
||||
"client_id": None,
|
||||
"client_secret": None,
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
@@ -203,8 +264,26 @@ def test_openapi_schema():
|
||||
"username": {"title": "Username", "type": "string"},
|
||||
"password": {"title": "Password", "type": "string"},
|
||||
"scope": {"title": "Scope", "type": "string", "default": ""},
|
||||
"client_id": {"title": "Client Id", "type": "string"},
|
||||
"client_secret": {"title": "Client Secret", "type": "string"},
|
||||
"client_id": IsDict(
|
||||
{
|
||||
"title": "Client Id",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Client Id", "type": "string"}
|
||||
),
|
||||
"client_secret": IsDict(
|
||||
{
|
||||
"title": "Client Secret",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Client Secret", "type": "string"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
@@ -64,76 +65,136 @@ def test_security_oauth2_password_bearer_no_header():
|
||||
assert response.json() == {"msg": "Create an account first"}
|
||||
|
||||
|
||||
required_params = {
|
||||
"detail": [
|
||||
def test_strict_login_None():
|
||||
response = client.post("/login", data=None)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "username"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "password"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
grant_type_required = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "username"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "password"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
grant_type_incorrect = {
|
||||
"detail": [
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": 'string does not match regex "password"',
|
||||
"type": "value_error.str.regex",
|
||||
"ctx": {"pattern": "password"},
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "username"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "password"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data,expected_status,expected_response",
|
||||
[
|
||||
(None, 422, required_params),
|
||||
({"username": "johndoe", "password": "secret"}, 422, grant_type_required),
|
||||
(
|
||||
{"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
|
||||
422,
|
||||
grant_type_incorrect,
|
||||
),
|
||||
(
|
||||
{"username": "johndoe", "password": "secret", "grant_type": "password"},
|
||||
200,
|
||||
{
|
||||
"grant_type": "password",
|
||||
"username": "johndoe",
|
||||
"password": "secret",
|
||||
"scopes": [],
|
||||
"client_id": None,
|
||||
"client_secret": None,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_strict_login(data, expected_status, expected_response):
|
||||
response = client.post("/login", data=data)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
def test_strict_login_no_grant_type():
|
||||
response = client.post("/login", data={"username": "johndoe", "password": "secret"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_strict_login_incorrect_grant_type():
|
||||
response = client.post(
|
||||
"/login",
|
||||
data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "string_pattern_mismatch",
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "String should match pattern 'password'",
|
||||
"input": "incorrect",
|
||||
"ctx": {"pattern": "password"},
|
||||
"url": match_pydantic_error_url("string_pattern_mismatch"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": 'string does not match regex "password"',
|
||||
"type": "value_error.str.regex",
|
||||
"ctx": {"pattern": "password"},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_strict_login_correct_correct_grant_type():
|
||||
response = client.post(
|
||||
"/login",
|
||||
data={"username": "johndoe", "password": "secret", "grant_type": "password"},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"grant_type": "password",
|
||||
"username": "johndoe",
|
||||
"password": "secret",
|
||||
"scopes": [],
|
||||
"client_id": None,
|
||||
"client_secret": None,
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
@@ -204,8 +265,26 @@ def test_openapi_schema():
|
||||
"username": {"title": "Username", "type": "string"},
|
||||
"password": {"title": "Password", "type": "string"},
|
||||
"scope": {"title": "Scope", "type": "string", "default": ""},
|
||||
"client_id": {"title": "Client Id", "type": "string"},
|
||||
"client_secret": {"title": "Client Secret", "type": "string"},
|
||||
"client_id": IsDict(
|
||||
{
|
||||
"title": "Client Id",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Client Id", "type": "string"}
|
||||
),
|
||||
"client_secret": IsDict(
|
||||
{
|
||||
"title": "Client Secret",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Client Secret", "type": "string"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
||||
@@ -12,7 +12,7 @@ class SubModel(BaseModel):
|
||||
|
||||
|
||||
class Model(BaseModel):
|
||||
x: Optional[int]
|
||||
x: Optional[int] = None
|
||||
sub: SubModel
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
@@ -98,13 +99,30 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"title": "Callback Url",
|
||||
"maxLength": 2083,
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"title": "Callback Url",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"minLength": 1,
|
||||
"maxLength": 2083,
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "Callback Url",
|
||||
"maxLength": 2083,
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
}
|
||||
),
|
||||
"name": "callback_url",
|
||||
"in": "query",
|
||||
}
|
||||
@@ -244,7 +262,16 @@ def test_openapi_schema():
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"title": "Id", "type": "string"},
|
||||
"title": {"title": "Title", "type": "string"},
|
||||
"title": IsDict(
|
||||
{
|
||||
"title": "Title",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Title", "type": "string"}
|
||||
),
|
||||
"customer": {"title": "Customer", "type": "string"},
|
||||
"total": {"title": "Total", "type": "number"},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import List, Tuple
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Form
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
@@ -126,16 +127,31 @@ def test_openapi_schema():
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"title": "Square",
|
||||
"maxItems": 2,
|
||||
"minItems": 2,
|
||||
"type": "array",
|
||||
"items": [
|
||||
{"$ref": "#/components/schemas/Coordinate"},
|
||||
{"$ref": "#/components/schemas/Coordinate"},
|
||||
],
|
||||
}
|
||||
"schema": IsDict(
|
||||
{
|
||||
"title": "Square",
|
||||
"maxItems": 2,
|
||||
"minItems": 2,
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{"$ref": "#/components/schemas/Coordinate"},
|
||||
{"$ref": "#/components/schemas/Coordinate"},
|
||||
],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "Square",
|
||||
"maxItems": 2,
|
||||
"minItems": 2,
|
||||
"type": "array",
|
||||
"items": [
|
||||
{"$ref": "#/components/schemas/Coordinate"},
|
||||
{"$ref": "#/components/schemas/Coordinate"},
|
||||
],
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@@ -198,13 +214,28 @@ def test_openapi_schema():
|
||||
"required": ["values"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"values": {
|
||||
"title": "Values",
|
||||
"maxItems": 2,
|
||||
"minItems": 2,
|
||||
"type": "array",
|
||||
"items": [{"type": "integer"}, {"type": "integer"}],
|
||||
}
|
||||
"values": IsDict(
|
||||
{
|
||||
"title": "Values",
|
||||
"maxItems": 2,
|
||||
"minItems": 2,
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{"type": "integer"},
|
||||
{"type": "integer"},
|
||||
],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "Values",
|
||||
"maxItems": 2,
|
||||
"minItems": 2,
|
||||
"type": "array",
|
||||
"items": [{"type": "integer"}, {"type": "integer"}],
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
"Coordinate": {
|
||||
@@ -235,12 +266,26 @@ def test_openapi_schema():
|
||||
"items": {
|
||||
"title": "Items",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"maxItems": 2,
|
||||
"minItems": 2,
|
||||
"type": "array",
|
||||
"items": [{"type": "string"}, {"type": "string"}],
|
||||
},
|
||||
"items": IsDict(
|
||||
{
|
||||
"maxItems": 2,
|
||||
"minItems": 2,
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{"type": "string"},
|
||||
{"type": "string"},
|
||||
],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"maxItems": 2,
|
||||
"minItems": 2,
|
||||
"type": "array",
|
||||
"items": [{"type": "string"}, {"type": "string"}],
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.additional_responses.tutorial002 import app
|
||||
@@ -64,7 +65,16 @@ def test_openapi_schema():
|
||||
},
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Img", "type": "boolean"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
||||
"title": "Img",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Img", "type": "boolean"}
|
||||
),
|
||||
"name": "img",
|
||||
"in": "query",
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.additional_responses.tutorial004 import app
|
||||
@@ -67,7 +68,16 @@ def test_openapi_schema():
|
||||
},
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Img", "type": "boolean"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
||||
"title": "Img",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Img", "type": "boolean"}
|
||||
),
|
||||
"name": "img",
|
||||
"in": "query",
|
||||
},
|
||||
|
||||
@@ -2,7 +2,11 @@ from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.async_sql_databases.tutorial001 import app
|
||||
|
||||
from ...utils import needs_pydanticv1
|
||||
|
||||
|
||||
# TODO: pv2 add version with Pydantic v2
|
||||
@needs_pydanticv1
|
||||
def test_create_read():
|
||||
with TestClient(app) as client:
|
||||
note = {"text": "Foo bar", "completed": False}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from dirty_equals import IsOneOf
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.behind_a_proxy.tutorial003 import app
|
||||
@@ -11,7 +12,7 @@ def test_main():
|
||||
assert response.json() == {"message": "Hello World", "root_path": "/api/v1"}
|
||||
|
||||
|
||||
def test_openapi():
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
@@ -19,9 +20,20 @@ def test_openapi():
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"servers": [
|
||||
{"url": "/api/v1"},
|
||||
{"url": "https://stag.example.com", "description": "Staging environment"},
|
||||
{
|
||||
"url": "https://prod.example.com",
|
||||
"url": IsOneOf(
|
||||
"https://stag.example.com/",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"https://stag.example.com",
|
||||
),
|
||||
"description": "Staging environment",
|
||||
},
|
||||
{
|
||||
"url": IsOneOf(
|
||||
"https://prod.example.com/",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"https://prod.example.com",
|
||||
),
|
||||
"description": "Production environment",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from dirty_equals import IsOneOf
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.behind_a_proxy.tutorial004 import app
|
||||
@@ -11,16 +12,27 @@ def test_main():
|
||||
assert response.json() == {"message": "Hello World", "root_path": "/api/v1"}
|
||||
|
||||
|
||||
def test_openapi():
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"servers": [
|
||||
{"url": "https://stag.example.com", "description": "Staging environment"},
|
||||
{
|
||||
"url": "https://prod.example.com",
|
||||
"url": IsOneOf(
|
||||
"https://stag.example.com/",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"https://stag.example.com",
|
||||
),
|
||||
"description": "Staging environment",
|
||||
},
|
||||
{
|
||||
"url": IsOneOf(
|
||||
"https://prod.example.com/",
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"https://prod.example.com",
|
||||
),
|
||||
"description": "Production environment",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,138 +1,368 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.bigger_applications.app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
|
||||
no_jessica = {
|
||||
"detail": [
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from docs_src.bigger_applications.app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
def test_users_token_jessica(client: TestClient):
|
||||
response = client.get("/users?token=jessica")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"username": "Rick"}, {"username": "Morty"}]
|
||||
|
||||
|
||||
def test_users_with_no_token(client: TestClient):
|
||||
response = client.get("/users")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected_status,expected_response,headers",
|
||||
[
|
||||
(
|
||||
"/users?token=jessica",
|
||||
200,
|
||||
[{"username": "Rick"}, {"username": "Morty"}],
|
||||
{},
|
||||
),
|
||||
("/users", 422, no_jessica, {}),
|
||||
("/users/foo?token=jessica", 200, {"username": "foo"}, {}),
|
||||
("/users/foo", 422, no_jessica, {}),
|
||||
("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}),
|
||||
("/users/me", 422, no_jessica, {}),
|
||||
(
|
||||
"/users?token=monica",
|
||||
400,
|
||||
{"detail": "No Jessica token provided"},
|
||||
{},
|
||||
),
|
||||
(
|
||||
"/items?token=jessica",
|
||||
200,
|
||||
{"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}},
|
||||
{"X-Token": "fake-super-secret-token"},
|
||||
),
|
||||
("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
|
||||
(
|
||||
"/items/plumbus?token=jessica",
|
||||
200,
|
||||
{"name": "Plumbus", "item_id": "plumbus"},
|
||||
{"X-Token": "fake-super-secret-token"},
|
||||
),
|
||||
(
|
||||
"/items/bar?token=jessica",
|
||||
404,
|
||||
{"detail": "Item not found"},
|
||||
{"X-Token": "fake-super-secret-token"},
|
||||
),
|
||||
("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
|
||||
(
|
||||
"/items?token=jessica",
|
||||
400,
|
||||
{"detail": "X-Token header invalid"},
|
||||
{"X-Token": "invalid"},
|
||||
),
|
||||
(
|
||||
"/items/bar?token=jessica",
|
||||
400,
|
||||
{"detail": "X-Token header invalid"},
|
||||
{"X-Token": "invalid"},
|
||||
),
|
||||
(
|
||||
"/items?token=jessica",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
{},
|
||||
),
|
||||
(
|
||||
"/items/plumbus?token=jessica",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
{},
|
||||
),
|
||||
("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}),
|
||||
("/", 422, no_jessica, {}),
|
||||
],
|
||||
)
|
||||
def test_get_path(path, expected_status, expected_response, headers):
|
||||
response = client.get(path, headers=headers)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
def test_users_foo_token_jessica(client: TestClient):
|
||||
response = client.get("/users/foo?token=jessica")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "foo"}
|
||||
|
||||
|
||||
def test_put_no_header():
|
||||
response = client.put("/items/foo")
|
||||
assert response.status_code == 422, response.text
|
||||
def test_users_foo_with_no_token(client: TestClient):
|
||||
response = client.get("/users/foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_users_me_token_jessica(client: TestClient):
|
||||
response = client.get("/users/me?token=jessica")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "fakecurrentuser"}
|
||||
|
||||
|
||||
def test_users_me_with_no_token(client: TestClient):
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_users_token_monica_with_no_jessica(client: TestClient):
|
||||
response = client.get("/users?token=monica")
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "No Jessica token provided"}
|
||||
|
||||
|
||||
def test_items_token_jessica(client: TestClient):
|
||||
response = client.get(
|
||||
"/items?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
"plumbus": {"name": "Plumbus"},
|
||||
"gun": {"name": "Portal Gun"},
|
||||
}
|
||||
|
||||
|
||||
def test_put_invalid_header():
|
||||
def test_items_with_no_token_jessica(client: TestClient):
|
||||
response = client.get("/items", headers={"X-Token": "fake-super-secret-token"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_items_plumbus_token_jessica(client: TestClient):
|
||||
response = client.get(
|
||||
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "Plumbus", "item_id": "plumbus"}
|
||||
|
||||
|
||||
def test_items_bar_token_jessica(client: TestClient):
|
||||
response = client.get(
|
||||
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "Item not found"}
|
||||
|
||||
|
||||
def test_items_plumbus_with_no_token(client: TestClient):
|
||||
response = client.get(
|
||||
"/items/plumbus", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_items_with_invalid_token(client: TestClient):
|
||||
response = client.get("/items?token=jessica", headers={"X-Token": "invalid"})
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "X-Token header invalid"}
|
||||
|
||||
|
||||
def test_items_bar_with_invalid_token(client: TestClient):
|
||||
response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"})
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "X-Token header invalid"}
|
||||
|
||||
|
||||
def test_items_with_missing_x_token_header(client: TestClient):
|
||||
response = client.get("/items?token=jessica")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_items_plumbus_with_missing_x_token_header(client: TestClient):
|
||||
response = client.get("/items/plumbus?token=jessica")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_root_token_jessica(client: TestClient):
|
||||
response = client.get("/?token=jessica")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Hello Bigger Applications!"}
|
||||
|
||||
|
||||
def test_root_with_no_token(client: TestClient):
|
||||
response = client.get("/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_put_no_header(client: TestClient):
|
||||
response = client.put("/items/foo")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_put_invalid_header(client: TestClient):
|
||||
response = client.put("/items/foo", headers={"X-Token": "invalid"})
|
||||
assert response.status_code == 400, response.text
|
||||
assert response.json() == {"detail": "X-Token header invalid"}
|
||||
|
||||
|
||||
def test_put():
|
||||
def test_put(client: TestClient):
|
||||
response = client.put(
|
||||
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
@@ -140,7 +370,7 @@ def test_put():
|
||||
assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"}
|
||||
|
||||
|
||||
def test_put_forbidden():
|
||||
def test_put_forbidden(client: TestClient):
|
||||
response = client.put(
|
||||
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
@@ -148,7 +378,7 @@ def test_put_forbidden():
|
||||
assert response.json() == {"detail": "You can only update the item: plumbus"}
|
||||
|
||||
|
||||
def test_admin():
|
||||
def test_admin(client: TestClient):
|
||||
response = client.post(
|
||||
"/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
@@ -156,13 +386,13 @@ def test_admin():
|
||||
assert response.json() == {"message": "Admin getting schwifty"}
|
||||
|
||||
|
||||
def test_admin_invalid_header():
|
||||
def test_admin_invalid_header(client: TestClient):
|
||||
response = client.post("/admin/", headers={"X-Token": "invalid"})
|
||||
assert response.status_code == 400, response.text
|
||||
assert response.json() == {"detail": "X-Token header invalid"}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
|
||||
@@ -1,138 +1,368 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.bigger_applications.app_an.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
|
||||
no_jessica = {
|
||||
"detail": [
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from docs_src.bigger_applications.app_an.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
def test_users_token_jessica(client: TestClient):
|
||||
response = client.get("/users?token=jessica")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"username": "Rick"}, {"username": "Morty"}]
|
||||
|
||||
|
||||
def test_users_with_no_token(client: TestClient):
|
||||
response = client.get("/users")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected_status,expected_response,headers",
|
||||
[
|
||||
(
|
||||
"/users?token=jessica",
|
||||
200,
|
||||
[{"username": "Rick"}, {"username": "Morty"}],
|
||||
{},
|
||||
),
|
||||
("/users", 422, no_jessica, {}),
|
||||
("/users/foo?token=jessica", 200, {"username": "foo"}, {}),
|
||||
("/users/foo", 422, no_jessica, {}),
|
||||
("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}),
|
||||
("/users/me", 422, no_jessica, {}),
|
||||
(
|
||||
"/users?token=monica",
|
||||
400,
|
||||
{"detail": "No Jessica token provided"},
|
||||
{},
|
||||
),
|
||||
(
|
||||
"/items?token=jessica",
|
||||
200,
|
||||
{"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}},
|
||||
{"X-Token": "fake-super-secret-token"},
|
||||
),
|
||||
("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
|
||||
(
|
||||
"/items/plumbus?token=jessica",
|
||||
200,
|
||||
{"name": "Plumbus", "item_id": "plumbus"},
|
||||
{"X-Token": "fake-super-secret-token"},
|
||||
),
|
||||
(
|
||||
"/items/bar?token=jessica",
|
||||
404,
|
||||
{"detail": "Item not found"},
|
||||
{"X-Token": "fake-super-secret-token"},
|
||||
),
|
||||
("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
|
||||
(
|
||||
"/items?token=jessica",
|
||||
400,
|
||||
{"detail": "X-Token header invalid"},
|
||||
{"X-Token": "invalid"},
|
||||
),
|
||||
(
|
||||
"/items/bar?token=jessica",
|
||||
400,
|
||||
{"detail": "X-Token header invalid"},
|
||||
{"X-Token": "invalid"},
|
||||
),
|
||||
(
|
||||
"/items?token=jessica",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
{},
|
||||
),
|
||||
(
|
||||
"/items/plumbus?token=jessica",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
{},
|
||||
),
|
||||
("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}),
|
||||
("/", 422, no_jessica, {}),
|
||||
],
|
||||
)
|
||||
def test_get_path(path, expected_status, expected_response, headers):
|
||||
response = client.get(path, headers=headers)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
def test_users_foo_token_jessica(client: TestClient):
|
||||
response = client.get("/users/foo?token=jessica")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "foo"}
|
||||
|
||||
|
||||
def test_put_no_header():
|
||||
response = client.put("/items/foo")
|
||||
assert response.status_code == 422, response.text
|
||||
def test_users_foo_with_no_token(client: TestClient):
|
||||
response = client.get("/users/foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_users_me_token_jessica(client: TestClient):
|
||||
response = client.get("/users/me?token=jessica")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "fakecurrentuser"}
|
||||
|
||||
|
||||
def test_users_me_with_no_token(client: TestClient):
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_users_token_monica_with_no_jessica(client: TestClient):
|
||||
response = client.get("/users?token=monica")
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "No Jessica token provided"}
|
||||
|
||||
|
||||
def test_items_token_jessica(client: TestClient):
|
||||
response = client.get(
|
||||
"/items?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
"plumbus": {"name": "Plumbus"},
|
||||
"gun": {"name": "Portal Gun"},
|
||||
}
|
||||
|
||||
|
||||
def test_put_invalid_header():
|
||||
def test_items_with_no_token_jessica(client: TestClient):
|
||||
response = client.get("/items", headers={"X-Token": "fake-super-secret-token"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_items_plumbus_token_jessica(client: TestClient):
|
||||
response = client.get(
|
||||
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "Plumbus", "item_id": "plumbus"}
|
||||
|
||||
|
||||
def test_items_bar_token_jessica(client: TestClient):
|
||||
response = client.get(
|
||||
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "Item not found"}
|
||||
|
||||
|
||||
def test_items_plumbus_with_no_token(client: TestClient):
|
||||
response = client.get(
|
||||
"/items/plumbus", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_items_with_invalid_token(client: TestClient):
|
||||
response = client.get("/items?token=jessica", headers={"X-Token": "invalid"})
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "X-Token header invalid"}
|
||||
|
||||
|
||||
def test_items_bar_with_invalid_token(client: TestClient):
|
||||
response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"})
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "X-Token header invalid"}
|
||||
|
||||
|
||||
def test_items_with_missing_x_token_header(client: TestClient):
|
||||
response = client.get("/items?token=jessica")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_items_plumbus_with_missing_x_token_header(client: TestClient):
|
||||
response = client.get("/items/plumbus?token=jessica")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_root_token_jessica(client: TestClient):
|
||||
response = client.get("/?token=jessica")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Hello Bigger Applications!"}
|
||||
|
||||
|
||||
def test_root_with_no_token(client: TestClient):
|
||||
response = client.get("/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_put_no_header(client: TestClient):
|
||||
response = client.put("/items/foo")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_put_invalid_header(client: TestClient):
|
||||
response = client.put("/items/foo", headers={"X-Token": "invalid"})
|
||||
assert response.status_code == 400, response.text
|
||||
assert response.json() == {"detail": "X-Token header invalid"}
|
||||
|
||||
|
||||
def test_put():
|
||||
def test_put(client: TestClient):
|
||||
response = client.put(
|
||||
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
@@ -140,7 +370,7 @@ def test_put():
|
||||
assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"}
|
||||
|
||||
|
||||
def test_put_forbidden():
|
||||
def test_put_forbidden(client: TestClient):
|
||||
response = client.put(
|
||||
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
@@ -148,7 +378,7 @@ def test_put_forbidden():
|
||||
assert response.json() == {"detail": "You can only update the item: plumbus"}
|
||||
|
||||
|
||||
def test_admin():
|
||||
def test_admin(client: TestClient):
|
||||
response = client.post(
|
||||
"/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
@@ -156,13 +386,13 @@ def test_admin():
|
||||
assert response.json() == {"message": "Admin getting schwifty"}
|
||||
|
||||
|
||||
def test_admin_invalid_header():
|
||||
def test_admin_invalid_header(client: TestClient):
|
||||
response = client.post("/admin/", headers={"X-Token": "invalid"})
|
||||
assert response.status_code == 400, response.text
|
||||
assert response.json() == {"detail": "X-Token header invalid"}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from ...utils import needs_py39
|
||||
|
||||
no_jessica = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
@@ -23,116 +15,366 @@ def get_client():
|
||||
|
||||
|
||||
@needs_py39
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected_status,expected_response,headers",
|
||||
[
|
||||
(
|
||||
"/users?token=jessica",
|
||||
200,
|
||||
[{"username": "Rick"}, {"username": "Morty"}],
|
||||
{},
|
||||
),
|
||||
("/users", 422, no_jessica, {}),
|
||||
("/users/foo?token=jessica", 200, {"username": "foo"}, {}),
|
||||
("/users/foo", 422, no_jessica, {}),
|
||||
("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}),
|
||||
("/users/me", 422, no_jessica, {}),
|
||||
(
|
||||
"/users?token=monica",
|
||||
400,
|
||||
{"detail": "No Jessica token provided"},
|
||||
{},
|
||||
),
|
||||
(
|
||||
"/items?token=jessica",
|
||||
200,
|
||||
{"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}},
|
||||
{"X-Token": "fake-super-secret-token"},
|
||||
),
|
||||
("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
|
||||
(
|
||||
"/items/plumbus?token=jessica",
|
||||
200,
|
||||
{"name": "Plumbus", "item_id": "plumbus"},
|
||||
{"X-Token": "fake-super-secret-token"},
|
||||
),
|
||||
(
|
||||
"/items/bar?token=jessica",
|
||||
404,
|
||||
{"detail": "Item not found"},
|
||||
{"X-Token": "fake-super-secret-token"},
|
||||
),
|
||||
("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
|
||||
(
|
||||
"/items?token=jessica",
|
||||
400,
|
||||
{"detail": "X-Token header invalid"},
|
||||
{"X-Token": "invalid"},
|
||||
),
|
||||
(
|
||||
"/items/bar?token=jessica",
|
||||
400,
|
||||
{"detail": "X-Token header invalid"},
|
||||
{"X-Token": "invalid"},
|
||||
),
|
||||
(
|
||||
"/items?token=jessica",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
{},
|
||||
),
|
||||
(
|
||||
"/items/plumbus?token=jessica",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
{},
|
||||
),
|
||||
("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}),
|
||||
("/", 422, no_jessica, {}),
|
||||
],
|
||||
)
|
||||
def test_get_path(
|
||||
path, expected_status, expected_response, headers, client: TestClient
|
||||
):
|
||||
response = client.get(path, headers=headers)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
def test_users_token_jessica(client: TestClient):
|
||||
response = client.get("/users?token=jessica")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"username": "Rick"}, {"username": "Morty"}]
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_users_with_no_token(client: TestClient):
|
||||
response = client.get("/users")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_users_foo_token_jessica(client: TestClient):
|
||||
response = client.get("/users/foo?token=jessica")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "foo"}
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_users_foo_with_no_token(client: TestClient):
|
||||
response = client.get("/users/foo")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_users_me_token_jessica(client: TestClient):
|
||||
response = client.get("/users/me?token=jessica")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "fakecurrentuser"}
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_users_me_with_no_token(client: TestClient):
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_users_token_monica_with_no_jessica(client: TestClient):
|
||||
response = client.get("/users?token=monica")
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "No Jessica token provided"}
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_items_token_jessica(client: TestClient):
|
||||
response = client.get(
|
||||
"/items?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"plumbus": {"name": "Plumbus"},
|
||||
"gun": {"name": "Portal Gun"},
|
||||
}
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_items_with_no_token_jessica(client: TestClient):
|
||||
response = client.get("/items", headers={"X-Token": "fake-super-secret-token"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_items_plumbus_token_jessica(client: TestClient):
|
||||
response = client.get(
|
||||
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "Plumbus", "item_id": "plumbus"}
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_items_bar_token_jessica(client: TestClient):
|
||||
response = client.get(
|
||||
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "Item not found"}
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_items_plumbus_with_no_token(client: TestClient):
|
||||
response = client.get(
|
||||
"/items/plumbus", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_items_with_invalid_token(client: TestClient):
|
||||
response = client.get("/items?token=jessica", headers={"X-Token": "invalid"})
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "X-Token header invalid"}
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_items_bar_with_invalid_token(client: TestClient):
|
||||
response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"})
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "X-Token header invalid"}
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_items_with_missing_x_token_header(client: TestClient):
|
||||
response = client.get("/items?token=jessica")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_items_plumbus_with_missing_x_token_header(client: TestClient):
|
||||
response = client.get("/items/plumbus?token=jessica")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_root_token_jessica(client: TestClient):
|
||||
response = client.get("/?token=jessica")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Hello Bigger Applications!"}
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_root_with_no_token(client: TestClient):
|
||||
response = client.get("/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_put_no_header(client: TestClient):
|
||||
response = client.put("/items/foo")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py39
|
||||
|
||||
@@ -1,134 +1,268 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.body.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
|
||||
price_missing = {
|
||||
"detail": [
|
||||
@pytest.fixture
|
||||
def client():
|
||||
from docs_src.body.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
def test_body_float(client: TestClient):
|
||||
response = client.post("/items/", json={"name": "Foo", "price": 50.5})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
}
|
||||
|
||||
|
||||
def test_post_with_str_float(client: TestClient):
|
||||
response = client.post("/items/", json={"name": "Foo", "price": "50.5"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
}
|
||||
|
||||
|
||||
def test_post_with_str_float_description(client: TestClient):
|
||||
response = client.post(
|
||||
"/items/", json={"name": "Foo", "price": "50.5", "description": "Some Foo"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": "Some Foo",
|
||||
"tax": None,
|
||||
}
|
||||
|
||||
|
||||
def test_post_with_str_float_description_tax(client: TestClient):
|
||||
response = client.post(
|
||||
"/items/",
|
||||
json={"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": "Some Foo",
|
||||
"tax": 0.3,
|
||||
}
|
||||
|
||||
|
||||
def test_post_with_only_name(client: TestClient):
|
||||
response = client.post("/items/", json={"name": "Foo"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"loc": ["body", "price"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "price"],
|
||||
"msg": "Field required",
|
||||
"input": {"name": "Foo"},
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
price_not_float = {
|
||||
"detail": [
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"loc": ["body", "price"],
|
||||
"msg": "value is not a valid float",
|
||||
"type": "type_error.float",
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "price"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
name_price_missing = {
|
||||
"detail": [
|
||||
|
||||
def test_post_with_only_name_price(client: TestClient):
|
||||
response = client.post("/items/", json={"name": "Foo", "price": "twenty"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"loc": ["body", "name"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
"detail": [
|
||||
{
|
||||
"type": "float_parsing",
|
||||
"loc": ["body", "price"],
|
||||
"msg": "Input should be a valid number, unable to parse string as an number",
|
||||
"input": "twenty",
|
||||
"url": match_pydantic_error_url("float_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"loc": ["body", "price"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
body_missing = {
|
||||
"detail": [
|
||||
{"loc": ["body"], "msg": "field required", "type": "value_error.missing"}
|
||||
]
|
||||
}
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "price"],
|
||||
"msg": "value is not a valid float",
|
||||
"type": "type_error.float",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/",
|
||||
{"name": "Foo", "price": 50.5},
|
||||
200,
|
||||
{"name": "Foo", "price": 50.5, "description": None, "tax": None},
|
||||
),
|
||||
(
|
||||
"/items/",
|
||||
{"name": "Foo", "price": "50.5"},
|
||||
200,
|
||||
{"name": "Foo", "price": 50.5, "description": None, "tax": None},
|
||||
),
|
||||
(
|
||||
"/items/",
|
||||
{"name": "Foo", "price": "50.5", "description": "Some Foo"},
|
||||
200,
|
||||
{"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": None},
|
||||
),
|
||||
(
|
||||
"/items/",
|
||||
{"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3},
|
||||
200,
|
||||
{"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": 0.3},
|
||||
),
|
||||
("/items/", {"name": "Foo"}, 422, price_missing),
|
||||
("/items/", {"name": "Foo", "price": "twenty"}, 422, price_not_float),
|
||||
("/items/", {}, 422, name_price_missing),
|
||||
("/items/", None, 422, body_missing),
|
||||
],
|
||||
)
|
||||
def test_post_body(path, body, expected_status, expected_response):
|
||||
response = client.post(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
def test_post_with_no_data(client: TestClient):
|
||||
response = client.post("/items/", json={})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "name"],
|
||||
"msg": "Field required",
|
||||
"input": {},
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "price"],
|
||||
"msg": "Field required",
|
||||
"input": {},
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "name"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "price"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_post_broken_body():
|
||||
def test_post_with_none(client: TestClient):
|
||||
response = client.post("/items/", json=None)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_post_broken_body(client: TestClient):
|
||||
response = client.post(
|
||||
"/items/",
|
||||
headers={"content-type": "application/json"},
|
||||
content="{some broken json}",
|
||||
)
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", 1],
|
||||
"msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)",
|
||||
"type": "value_error.jsondecode",
|
||||
"ctx": {
|
||||
"msg": "Expecting property name enclosed in double quotes",
|
||||
"doc": "{some broken json}",
|
||||
"pos": 1,
|
||||
"lineno": 1,
|
||||
"colno": 2,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "json_invalid",
|
||||
"loc": ["body", 1],
|
||||
"msg": "JSON decode error",
|
||||
"input": {},
|
||||
"ctx": {
|
||||
"error": "Expecting property name enclosed in double quotes"
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", 1],
|
||||
"msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)",
|
||||
"type": "value_error.jsondecode",
|
||||
"ctx": {
|
||||
"msg": "Expecting property name enclosed in double quotes",
|
||||
"doc": "{some broken json}",
|
||||
"pos": 1,
|
||||
"lineno": 1,
|
||||
"colno": 2,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_post_form_for_json():
|
||||
def test_post_form_for_json(client: TestClient):
|
||||
response = client.post("/items/", data={"name": "Foo", "price": 50.5})
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "value is not a valid dict",
|
||||
"type": "type_error.dict",
|
||||
}
|
||||
]
|
||||
}
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "dict_attributes_type",
|
||||
"loc": ["body"],
|
||||
"msg": "Input should be a valid dictionary or instance to extract fields from",
|
||||
"input": "name=Foo&price=50.5",
|
||||
"url": match_pydantic_error_url("dict_attributes_type"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "value is not a valid dict",
|
||||
"type": "type_error.dict",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_explicit_content_type():
|
||||
def test_explicit_content_type(client: TestClient):
|
||||
response = client.post(
|
||||
"/items/",
|
||||
content='{"name": "Foo", "price": 50.5}',
|
||||
@@ -137,7 +271,7 @@ def test_explicit_content_type():
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
|
||||
def test_geo_json():
|
||||
def test_geo_json(client: TestClient):
|
||||
response = client.post(
|
||||
"/items/",
|
||||
content='{"name": "Foo", "price": 50.5}',
|
||||
@@ -146,7 +280,7 @@ def test_geo_json():
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
|
||||
def test_no_content_type_is_json():
|
||||
def test_no_content_type_is_json(client: TestClient):
|
||||
response = client.post(
|
||||
"/items/",
|
||||
content='{"name": "Foo", "price": 50.5}',
|
||||
@@ -160,43 +294,104 @@ def test_no_content_type_is_json():
|
||||
}
|
||||
|
||||
|
||||
def test_wrong_headers():
|
||||
def test_wrong_headers(client: TestClient):
|
||||
data = '{"name": "Foo", "price": 50.5}'
|
||||
invalid_dict = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "value is not a valid dict",
|
||||
"type": "type_error.dict",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
"/items/", content=data, headers={"Content-Type": "text/plain"}
|
||||
)
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == invalid_dict
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "dict_attributes_type",
|
||||
"loc": ["body"],
|
||||
"msg": "Input should be a valid dictionary or instance to extract fields from",
|
||||
"input": '{"name": "Foo", "price": 50.5}',
|
||||
"url": match_pydantic_error_url(
|
||||
"dict_attributes_type"
|
||||
), # "https://errors.pydantic.dev/0.38.0/v/dict_attributes_type",
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "value is not a valid dict",
|
||||
"type": "type_error.dict",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/items/", content=data, headers={"Content-Type": "application/geo+json-seq"}
|
||||
)
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == invalid_dict
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "dict_attributes_type",
|
||||
"loc": ["body"],
|
||||
"msg": "Input should be a valid dictionary or instance to extract fields from",
|
||||
"input": '{"name": "Foo", "price": 50.5}',
|
||||
"url": match_pydantic_error_url("dict_attributes_type"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "value is not a valid dict",
|
||||
"type": "type_error.dict",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
response = client.post(
|
||||
"/items/", content=data, headers={"Content-Type": "application/not-really-json"}
|
||||
)
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == invalid_dict
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "dict_attributes_type",
|
||||
"loc": ["body"],
|
||||
"msg": "Input should be a valid dictionary or instance to extract fields from",
|
||||
"input": '{"name": "Foo", "price": 50.5}',
|
||||
"url": match_pydantic_error_url("dict_attributes_type"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "value is not a valid dict",
|
||||
"type": "type_error.dict",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_other_exceptions():
|
||||
def test_other_exceptions(client: TestClient):
|
||||
with patch("json.loads", side_effect=Exception):
|
||||
response = client.post("/items/", json={"test": "test2"})
|
||||
assert response.status_code == 400, response.text
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
@@ -243,8 +438,26 @@ def test_openapi_schema():
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from ...utils import needs_py310
|
||||
|
||||
@@ -14,86 +16,189 @@ def client():
|
||||
return client
|
||||
|
||||
|
||||
price_missing = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "price"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
price_not_float = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "price"],
|
||||
"msg": "value is not a valid float",
|
||||
"type": "type_error.float",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
name_price_missing = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "name"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "price"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
body_missing = {
|
||||
"detail": [
|
||||
{"loc": ["body"], "msg": "field required", "type": "value_error.missing"}
|
||||
]
|
||||
}
|
||||
@needs_py310
|
||||
def test_body_float(client: TestClient):
|
||||
response = client.post("/items/", json={"name": "Foo", "price": 50.5})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
}
|
||||
|
||||
|
||||
@needs_py310
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/",
|
||||
{"name": "Foo", "price": 50.5},
|
||||
200,
|
||||
{"name": "Foo", "price": 50.5, "description": None, "tax": None},
|
||||
),
|
||||
(
|
||||
"/items/",
|
||||
{"name": "Foo", "price": "50.5"},
|
||||
200,
|
||||
{"name": "Foo", "price": 50.5, "description": None, "tax": None},
|
||||
),
|
||||
(
|
||||
"/items/",
|
||||
{"name": "Foo", "price": "50.5", "description": "Some Foo"},
|
||||
200,
|
||||
{"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": None},
|
||||
),
|
||||
(
|
||||
"/items/",
|
||||
{"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3},
|
||||
200,
|
||||
{"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": 0.3},
|
||||
),
|
||||
("/items/", {"name": "Foo"}, 422, price_missing),
|
||||
("/items/", {"name": "Foo", "price": "twenty"}, 422, price_not_float),
|
||||
("/items/", {}, 422, name_price_missing),
|
||||
("/items/", None, 422, body_missing),
|
||||
],
|
||||
)
|
||||
def test_post_body(path, body, expected_status, expected_response, client: TestClient):
|
||||
response = client.post(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
def test_post_with_str_float(client: TestClient):
|
||||
response = client.post("/items/", json={"name": "Foo", "price": "50.5"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
}
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_with_str_float_description(client: TestClient):
|
||||
response = client.post(
|
||||
"/items/", json={"name": "Foo", "price": "50.5", "description": "Some Foo"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": "Some Foo",
|
||||
"tax": None,
|
||||
}
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_with_str_float_description_tax(client: TestClient):
|
||||
response = client.post(
|
||||
"/items/",
|
||||
json={"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": "Some Foo",
|
||||
"tax": 0.3,
|
||||
}
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_with_only_name(client: TestClient):
|
||||
response = client.post("/items/", json={"name": "Foo"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "price"],
|
||||
"msg": "Field required",
|
||||
"input": {"name": "Foo"},
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "price"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_with_only_name_price(client: TestClient):
|
||||
response = client.post("/items/", json={"name": "Foo", "price": "twenty"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "float_parsing",
|
||||
"loc": ["body", "price"],
|
||||
"msg": "Input should be a valid number, unable to parse string as an number",
|
||||
"input": "twenty",
|
||||
"url": match_pydantic_error_url("float_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "price"],
|
||||
"msg": "value is not a valid float",
|
||||
"type": "type_error.float",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_with_no_data(client: TestClient):
|
||||
response = client.post("/items/", json={})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "name"],
|
||||
"msg": "Field required",
|
||||
"input": {},
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "price"],
|
||||
"msg": "Field required",
|
||||
"input": {},
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "name"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "price"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_with_none(client: TestClient):
|
||||
response = client.post("/items/", json=None)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
@@ -104,37 +209,69 @@ def test_post_broken_body(client: TestClient):
|
||||
content="{some broken json}",
|
||||
)
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", 1],
|
||||
"msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)",
|
||||
"type": "value_error.jsondecode",
|
||||
"ctx": {
|
||||
"msg": "Expecting property name enclosed in double quotes",
|
||||
"doc": "{some broken json}",
|
||||
"pos": 1,
|
||||
"lineno": 1,
|
||||
"colno": 2,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "json_invalid",
|
||||
"loc": ["body", 1],
|
||||
"msg": "JSON decode error",
|
||||
"input": {},
|
||||
"ctx": {
|
||||
"error": "Expecting property name enclosed in double quotes"
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", 1],
|
||||
"msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)",
|
||||
"type": "value_error.jsondecode",
|
||||
"ctx": {
|
||||
"msg": "Expecting property name enclosed in double quotes",
|
||||
"doc": "{some broken json}",
|
||||
"pos": 1,
|
||||
"lineno": 1,
|
||||
"colno": 2,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_form_for_json(client: TestClient):
|
||||
response = client.post("/items/", data={"name": "Foo", "price": 50.5})
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "value is not a valid dict",
|
||||
"type": "type_error.dict",
|
||||
}
|
||||
]
|
||||
}
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "dict_attributes_type",
|
||||
"loc": ["body"],
|
||||
"msg": "Input should be a valid dictionary or instance to extract fields from",
|
||||
"input": "name=Foo&price=50.5",
|
||||
"url": match_pydantic_error_url("dict_attributes_type"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "value is not a valid dict",
|
||||
"type": "type_error.dict",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
@@ -175,32 +312,91 @@ def test_no_content_type_is_json(client: TestClient):
|
||||
@needs_py310
|
||||
def test_wrong_headers(client: TestClient):
|
||||
data = '{"name": "Foo", "price": 50.5}'
|
||||
invalid_dict = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "value is not a valid dict",
|
||||
"type": "type_error.dict",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
"/items/", content=data, headers={"Content-Type": "text/plain"}
|
||||
)
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == invalid_dict
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "dict_attributes_type",
|
||||
"loc": ["body"],
|
||||
"msg": "Input should be a valid dictionary or instance to extract fields from",
|
||||
"input": '{"name": "Foo", "price": 50.5}',
|
||||
"url": match_pydantic_error_url("dict_attributes_type"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "value is not a valid dict",
|
||||
"type": "type_error.dict",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/items/", content=data, headers={"Content-Type": "application/geo+json-seq"}
|
||||
)
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == invalid_dict
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "dict_attributes_type",
|
||||
"loc": ["body"],
|
||||
"msg": "Input should be a valid dictionary or instance to extract fields from",
|
||||
"input": '{"name": "Foo", "price": 50.5}',
|
||||
"url": match_pydantic_error_url("dict_attributes_type"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "value is not a valid dict",
|
||||
"type": "type_error.dict",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
response = client.post(
|
||||
"/items/", content=data, headers={"Content-Type": "application/not-really-json"}
|
||||
)
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == invalid_dict
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "dict_attributes_type",
|
||||
"loc": ["body"],
|
||||
"msg": "Input should be a valid dictionary or instance to extract fields from",
|
||||
"input": '{"name": "Foo", "price": 50.5}',
|
||||
"url": match_pydantic_error_url("dict_attributes_type"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "value is not a valid dict",
|
||||
"type": "type_error.dict",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
@@ -258,8 +454,26 @@ def test_openapi_schema(client: TestClient):
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
||||
@@ -1,66 +1,82 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.body_fields.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
|
||||
price_not_greater = {
|
||||
"detail": [
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from docs_src.body_fields.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
def test_items_5(client: TestClient):
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 5,
|
||||
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
|
||||
}
|
||||
|
||||
|
||||
def test_items_6(client: TestClient):
|
||||
response = client.put(
|
||||
"/items/6",
|
||||
json={
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": "5.4",
|
||||
}
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 6,
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": 5.4,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_invalid_price(client: TestClient):
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"ctx": {"limit_value": 0},
|
||||
"loc": ["body", "item", "price"],
|
||||
"msg": "ensure this value is greater than 0",
|
||||
"type": "value_error.number.not_gt",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/5",
|
||||
{"item": {"name": "Foo", "price": 3.0}},
|
||||
200,
|
||||
{
|
||||
"item_id": 5,
|
||||
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items/6",
|
||||
{
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": "5.4",
|
||||
"detail": [
|
||||
{
|
||||
"type": "greater_than",
|
||||
"loc": ["body", "item", "price"],
|
||||
"msg": "Input should be greater than 0",
|
||||
"input": -3.0,
|
||||
"ctx": {"gt": 0.0},
|
||||
"url": match_pydantic_error_url("greater_than"),
|
||||
}
|
||||
},
|
||||
200,
|
||||
{
|
||||
"item_id": 6,
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": 5.4,
|
||||
},
|
||||
},
|
||||
),
|
||||
("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater),
|
||||
],
|
||||
)
|
||||
def test(path, body, expected_status, expected_response):
|
||||
response = client.put(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"limit_value": 0},
|
||||
"loc": ["body", "item", "price"],
|
||||
"msg": "ensure this value is greater than 0",
|
||||
"type": "value_error.number.not_gt",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
@@ -116,18 +132,39 @@ def test_openapi_schema():
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": {
|
||||
"title": "The description of the item",
|
||||
"maxLength": 300,
|
||||
"type": "string",
|
||||
},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "The description of the item",
|
||||
"anyOf": [
|
||||
{"maxLength": 300, "type": "string"},
|
||||
{"type": "null"},
|
||||
],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "The description of the item",
|
||||
"maxLength": 300,
|
||||
"type": "string",
|
||||
}
|
||||
),
|
||||
"price": {
|
||||
"title": "Price",
|
||||
"exclusiveMinimum": 0.0,
|
||||
"type": "number",
|
||||
"description": "The price must be greater than zero",
|
||||
},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"Body_update_item_items__item_id__put": {
|
||||
|
||||
@@ -1,66 +1,82 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.body_fields.tutorial001_an import app
|
||||
|
||||
client = TestClient(app)
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
|
||||
price_not_greater = {
|
||||
"detail": [
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from docs_src.body_fields.tutorial001_an import app
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
def test_items_5(client: TestClient):
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 5,
|
||||
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
|
||||
}
|
||||
|
||||
|
||||
def test_items_6(client: TestClient):
|
||||
response = client.put(
|
||||
"/items/6",
|
||||
json={
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": "5.4",
|
||||
}
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 6,
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": 5.4,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_invalid_price(client: TestClient):
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"ctx": {"limit_value": 0},
|
||||
"loc": ["body", "item", "price"],
|
||||
"msg": "ensure this value is greater than 0",
|
||||
"type": "value_error.number.not_gt",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/5",
|
||||
{"item": {"name": "Foo", "price": 3.0}},
|
||||
200,
|
||||
{
|
||||
"item_id": 5,
|
||||
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items/6",
|
||||
{
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": "5.4",
|
||||
"detail": [
|
||||
{
|
||||
"type": "greater_than",
|
||||
"loc": ["body", "item", "price"],
|
||||
"msg": "Input should be greater than 0",
|
||||
"input": -3.0,
|
||||
"ctx": {"gt": 0.0},
|
||||
"url": match_pydantic_error_url("greater_than"),
|
||||
}
|
||||
},
|
||||
200,
|
||||
{
|
||||
"item_id": 6,
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": 5.4,
|
||||
},
|
||||
},
|
||||
),
|
||||
("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater),
|
||||
],
|
||||
)
|
||||
def test(path, body, expected_status, expected_response):
|
||||
response = client.put(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"limit_value": 0},
|
||||
"loc": ["body", "item", "price"],
|
||||
"msg": "ensure this value is greater than 0",
|
||||
"type": "value_error.number.not_gt",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
@@ -116,18 +132,39 @@ def test_openapi_schema():
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": {
|
||||
"title": "The description of the item",
|
||||
"maxLength": 300,
|
||||
"type": "string",
|
||||
},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "The description of the item",
|
||||
"anyOf": [
|
||||
{"maxLength": 300, "type": "string"},
|
||||
{"type": "null"},
|
||||
],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "The description of the item",
|
||||
"maxLength": 300,
|
||||
"type": "string",
|
||||
}
|
||||
),
|
||||
"price": {
|
||||
"title": "Price",
|
||||
"exclusiveMinimum": 0.0,
|
||||
"type": "number",
|
||||
"description": "The price must be greater than zero",
|
||||
},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"Body_update_item_items__item_id__put": {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from ...utils import needs_py310
|
||||
|
||||
@@ -12,59 +14,71 @@ def get_client():
|
||||
return client
|
||||
|
||||
|
||||
price_not_greater = {
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"limit_value": 0},
|
||||
"loc": ["body", "item", "price"],
|
||||
"msg": "ensure this value is greater than 0",
|
||||
"type": "value_error.number.not_gt",
|
||||
}
|
||||
]
|
||||
}
|
||||
@needs_py310
|
||||
def test_items_5(client: TestClient):
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 5,
|
||||
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
|
||||
}
|
||||
|
||||
|
||||
@needs_py310
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/5",
|
||||
{"item": {"name": "Foo", "price": 3.0}},
|
||||
200,
|
||||
{
|
||||
"item_id": 5,
|
||||
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items/6",
|
||||
{
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": "5.4",
|
||||
def test_items_6(client: TestClient):
|
||||
response = client.put(
|
||||
"/items/6",
|
||||
json={
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": "5.4",
|
||||
}
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 6,
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": 5.4,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_invalid_price(client: TestClient):
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "greater_than",
|
||||
"loc": ["body", "item", "price"],
|
||||
"msg": "Input should be greater than 0",
|
||||
"input": -3.0,
|
||||
"ctx": {"gt": 0.0},
|
||||
"url": match_pydantic_error_url("greater_than"),
|
||||
}
|
||||
},
|
||||
200,
|
||||
{
|
||||
"item_id": 6,
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": 5.4,
|
||||
},
|
||||
},
|
||||
),
|
||||
("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater),
|
||||
],
|
||||
)
|
||||
def test(path, body, expected_status, expected_response, client: TestClient):
|
||||
response = client.put(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"limit_value": 0},
|
||||
"loc": ["body", "item", "price"],
|
||||
"msg": "ensure this value is greater than 0",
|
||||
"type": "value_error.number.not_gt",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
@@ -124,18 +138,39 @@ def test_openapi_schema(client: TestClient):
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": {
|
||||
"title": "The description of the item",
|
||||
"maxLength": 300,
|
||||
"type": "string",
|
||||
},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "The description of the item",
|
||||
"anyOf": [
|
||||
{"maxLength": 300, "type": "string"},
|
||||
{"type": "null"},
|
||||
],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "The description of the item",
|
||||
"maxLength": 300,
|
||||
"type": "string",
|
||||
}
|
||||
),
|
||||
"price": {
|
||||
"title": "Price",
|
||||
"exclusiveMinimum": 0.0,
|
||||
"type": "number",
|
||||
"description": "The price must be greater than zero",
|
||||
},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"Body_update_item_items__item_id__put": {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from ...utils import needs_py39
|
||||
|
||||
@@ -12,59 +14,71 @@ def get_client():
|
||||
return client
|
||||
|
||||
|
||||
price_not_greater = {
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"limit_value": 0},
|
||||
"loc": ["body", "item", "price"],
|
||||
"msg": "ensure this value is greater than 0",
|
||||
"type": "value_error.number.not_gt",
|
||||
}
|
||||
]
|
||||
}
|
||||
@needs_py39
|
||||
def test_items_5(client: TestClient):
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 5,
|
||||
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
|
||||
}
|
||||
|
||||
|
||||
@needs_py39
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/5",
|
||||
{"item": {"name": "Foo", "price": 3.0}},
|
||||
200,
|
||||
{
|
||||
"item_id": 5,
|
||||
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items/6",
|
||||
{
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": "5.4",
|
||||
def test_items_6(client: TestClient):
|
||||
response = client.put(
|
||||
"/items/6",
|
||||
json={
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": "5.4",
|
||||
}
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 6,
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": 5.4,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_invalid_price(client: TestClient):
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "greater_than",
|
||||
"loc": ["body", "item", "price"],
|
||||
"msg": "Input should be greater than 0",
|
||||
"input": -3.0,
|
||||
"ctx": {"gt": 0.0},
|
||||
"url": match_pydantic_error_url("greater_than"),
|
||||
}
|
||||
},
|
||||
200,
|
||||
{
|
||||
"item_id": 6,
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": 5.4,
|
||||
},
|
||||
},
|
||||
),
|
||||
("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater),
|
||||
],
|
||||
)
|
||||
def test(path, body, expected_status, expected_response, client: TestClient):
|
||||
response = client.put(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"limit_value": 0},
|
||||
"loc": ["body", "item", "price"],
|
||||
"msg": "ensure this value is greater than 0",
|
||||
"type": "value_error.number.not_gt",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py39
|
||||
@@ -124,18 +138,39 @@ def test_openapi_schema(client: TestClient):
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": {
|
||||
"title": "The description of the item",
|
||||
"maxLength": 300,
|
||||
"type": "string",
|
||||
},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "The description of the item",
|
||||
"anyOf": [
|
||||
{"maxLength": 300, "type": "string"},
|
||||
{"type": "null"},
|
||||
],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "The description of the item",
|
||||
"maxLength": 300,
|
||||
"type": "string",
|
||||
}
|
||||
),
|
||||
"price": {
|
||||
"title": "Price",
|
||||
"exclusiveMinimum": 0.0,
|
||||
"type": "number",
|
||||
"description": "The price must be greater than zero",
|
||||
},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"Body_update_item_items__item_id__put": {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from ...utils import needs_py310
|
||||
|
||||
@@ -12,59 +14,71 @@ def get_client():
|
||||
return client
|
||||
|
||||
|
||||
price_not_greater = {
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"limit_value": 0},
|
||||
"loc": ["body", "item", "price"],
|
||||
"msg": "ensure this value is greater than 0",
|
||||
"type": "value_error.number.not_gt",
|
||||
}
|
||||
]
|
||||
}
|
||||
@needs_py310
|
||||
def test_items_5(client: TestClient):
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 5,
|
||||
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
|
||||
}
|
||||
|
||||
|
||||
@needs_py310
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/5",
|
||||
{"item": {"name": "Foo", "price": 3.0}},
|
||||
200,
|
||||
{
|
||||
"item_id": 5,
|
||||
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items/6",
|
||||
{
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": "5.4",
|
||||
def test_items_6(client: TestClient):
|
||||
response = client.put(
|
||||
"/items/6",
|
||||
json={
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": "5.4",
|
||||
}
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 6,
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": 5.4,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_invalid_price(client: TestClient):
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "greater_than",
|
||||
"loc": ["body", "item", "price"],
|
||||
"msg": "Input should be greater than 0",
|
||||
"input": -3.0,
|
||||
"ctx": {"gt": 0.0},
|
||||
"url": match_pydantic_error_url("greater_than"),
|
||||
}
|
||||
},
|
||||
200,
|
||||
{
|
||||
"item_id": 6,
|
||||
"item": {
|
||||
"name": "Bar",
|
||||
"price": 0.2,
|
||||
"description": "Some bar",
|
||||
"tax": 5.4,
|
||||
},
|
||||
},
|
||||
),
|
||||
("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater),
|
||||
],
|
||||
)
|
||||
def test(path, body, expected_status, expected_response, client: TestClient):
|
||||
response = client.put(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"limit_value": 0},
|
||||
"loc": ["body", "item", "price"],
|
||||
"msg": "ensure this value is greater than 0",
|
||||
"type": "value_error.number.not_gt",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
@@ -124,18 +138,39 @@ def test_openapi_schema(client: TestClient):
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": {
|
||||
"title": "The description of the item",
|
||||
"maxLength": 300,
|
||||
"type": "string",
|
||||
},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "The description of the item",
|
||||
"anyOf": [
|
||||
{"maxLength": 300, "type": "string"},
|
||||
{"type": "null"},
|
||||
],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "The description of the item",
|
||||
"maxLength": 300,
|
||||
"type": "string",
|
||||
}
|
||||
),
|
||||
"price": {
|
||||
"title": "Price",
|
||||
"exclusiveMinimum": 0.0,
|
||||
"type": "number",
|
||||
"description": "The price must be greater than zero",
|
||||
},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"Body_update_item_items__item_id__put": {
|
||||
|
||||
@@ -1,52 +1,74 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.body_multiple_params.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
|
||||
item_id_not_int = {
|
||||
"detail": [
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from docs_src.body_multiple_params.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
def test_post_body_q_bar_content(client: TestClient):
|
||||
response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 5,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
},
|
||||
"q": "bar",
|
||||
}
|
||||
|
||||
|
||||
def test_post_no_body_q_bar(client: TestClient):
|
||||
response = client.put("/items/5?q=bar", json=None)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": 5, "q": "bar"}
|
||||
|
||||
|
||||
def test_post_no_body(client: TestClient):
|
||||
response = client.put("/items/5", json=None)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": 5}
|
||||
|
||||
|
||||
def test_post_id_foo(client: TestClient):
|
||||
response = client.put("/items/foo", json=None)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"loc": ["path", "item_id"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["path", "item_id"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "foo",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["path", "item_id"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/5?q=bar",
|
||||
{"name": "Foo", "price": 50.5},
|
||||
200,
|
||||
{
|
||||
"item_id": 5,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
},
|
||||
"q": "bar",
|
||||
},
|
||||
),
|
||||
("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}),
|
||||
("/items/5", None, 200, {"item_id": 5}),
|
||||
("/items/foo", None, 422, item_id_not_int),
|
||||
],
|
||||
)
|
||||
def test_post_body(path, body, expected_status, expected_response):
|
||||
response = client.put(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
@@ -87,7 +109,16 @@ def test_openapi_schema():
|
||||
},
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Q", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Q",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Q", "type": "string"}
|
||||
),
|
||||
"name": "q",
|
||||
"in": "query",
|
||||
},
|
||||
@@ -95,7 +126,19 @@ def test_openapi_schema():
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{"$ref": "#/components/schemas/Item"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "Item",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"$ref": "#/components/schemas/Item"}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -110,9 +153,27 @@ def test_openapi_schema():
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
||||
@@ -1,52 +1,74 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.body_multiple_params.tutorial001_an import app
|
||||
|
||||
client = TestClient(app)
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
|
||||
item_id_not_int = {
|
||||
"detail": [
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from docs_src.body_multiple_params.tutorial001_an import app
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
def test_post_body_q_bar_content(client: TestClient):
|
||||
response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 5,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
},
|
||||
"q": "bar",
|
||||
}
|
||||
|
||||
|
||||
def test_post_no_body_q_bar(client: TestClient):
|
||||
response = client.put("/items/5?q=bar", json=None)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": 5, "q": "bar"}
|
||||
|
||||
|
||||
def test_post_no_body(client: TestClient):
|
||||
response = client.put("/items/5", json=None)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": 5}
|
||||
|
||||
|
||||
def test_post_id_foo(client: TestClient):
|
||||
response = client.put("/items/foo", json=None)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"loc": ["path", "item_id"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["path", "item_id"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "foo",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["path", "item_id"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/5?q=bar",
|
||||
{"name": "Foo", "price": 50.5},
|
||||
200,
|
||||
{
|
||||
"item_id": 5,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
},
|
||||
"q": "bar",
|
||||
},
|
||||
),
|
||||
("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}),
|
||||
("/items/5", None, 200, {"item_id": 5}),
|
||||
("/items/foo", None, 422, item_id_not_int),
|
||||
],
|
||||
)
|
||||
def test_post_body(path, body, expected_status, expected_response):
|
||||
response = client.put(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
@@ -87,7 +109,16 @@ def test_openapi_schema():
|
||||
},
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Q", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Q",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Q", "type": "string"}
|
||||
),
|
||||
"name": "q",
|
||||
"in": "query",
|
||||
},
|
||||
@@ -95,7 +126,19 @@ def test_openapi_schema():
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{"$ref": "#/components/schemas/Item"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "Item",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"$ref": "#/components/schemas/Item"}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -110,9 +153,27 @@ def test_openapi_schema():
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from ...utils import needs_py310
|
||||
|
||||
@@ -12,45 +14,64 @@ def get_client():
|
||||
return client
|
||||
|
||||
|
||||
item_id_not_int = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["path", "item_id"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
@needs_py310
|
||||
def test_post_body_q_bar_content(client: TestClient):
|
||||
response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 5,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
},
|
||||
"q": "bar",
|
||||
}
|
||||
|
||||
|
||||
@needs_py310
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/5?q=bar",
|
||||
{"name": "Foo", "price": 50.5},
|
||||
200,
|
||||
{
|
||||
"item_id": 5,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
},
|
||||
"q": "bar",
|
||||
},
|
||||
),
|
||||
("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}),
|
||||
("/items/5", None, 200, {"item_id": 5}),
|
||||
("/items/foo", None, 422, item_id_not_int),
|
||||
],
|
||||
)
|
||||
def test_post_body(path, body, expected_status, expected_response, client: TestClient):
|
||||
response = client.put(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
def test_post_no_body_q_bar(client: TestClient):
|
||||
response = client.put("/items/5?q=bar", json=None)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": 5, "q": "bar"}
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_no_body(client: TestClient):
|
||||
response = client.put("/items/5", json=None)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": 5}
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_id_foo(client: TestClient):
|
||||
response = client.put("/items/foo", json=None)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["path", "item_id"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "foo",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["path", "item_id"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
@@ -95,7 +116,16 @@ def test_openapi_schema(client: TestClient):
|
||||
},
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Q", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Q",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Q", "type": "string"}
|
||||
),
|
||||
"name": "q",
|
||||
"in": "query",
|
||||
},
|
||||
@@ -103,7 +133,19 @@ def test_openapi_schema(client: TestClient):
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{"$ref": "#/components/schemas/Item"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "Item",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"$ref": "#/components/schemas/Item"}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -118,9 +160,27 @@ def test_openapi_schema(client: TestClient):
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from ...utils import needs_py39
|
||||
|
||||
@@ -12,45 +14,64 @@ def get_client():
|
||||
return client
|
||||
|
||||
|
||||
item_id_not_int = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["path", "item_id"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
@needs_py39
|
||||
def test_post_body_q_bar_content(client: TestClient):
|
||||
response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 5,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
},
|
||||
"q": "bar",
|
||||
}
|
||||
|
||||
|
||||
@needs_py39
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/5?q=bar",
|
||||
{"name": "Foo", "price": 50.5},
|
||||
200,
|
||||
{
|
||||
"item_id": 5,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
},
|
||||
"q": "bar",
|
||||
},
|
||||
),
|
||||
("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}),
|
||||
("/items/5", None, 200, {"item_id": 5}),
|
||||
("/items/foo", None, 422, item_id_not_int),
|
||||
],
|
||||
)
|
||||
def test_post_body(path, body, expected_status, expected_response, client: TestClient):
|
||||
response = client.put(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
def test_post_no_body_q_bar(client: TestClient):
|
||||
response = client.put("/items/5?q=bar", json=None)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": 5, "q": "bar"}
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_post_no_body(client: TestClient):
|
||||
response = client.put("/items/5", json=None)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": 5}
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_post_id_foo(client: TestClient):
|
||||
response = client.put("/items/foo", json=None)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["path", "item_id"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "foo",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["path", "item_id"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py39
|
||||
@@ -95,7 +116,16 @@ def test_openapi_schema(client: TestClient):
|
||||
},
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Q", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Q",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Q", "type": "string"}
|
||||
),
|
||||
"name": "q",
|
||||
"in": "query",
|
||||
},
|
||||
@@ -103,7 +133,19 @@ def test_openapi_schema(client: TestClient):
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{"$ref": "#/components/schemas/Item"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "Item",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"$ref": "#/components/schemas/Item"}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -118,9 +160,27 @@ def test_openapi_schema(client: TestClient):
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from ...utils import needs_py310
|
||||
|
||||
@@ -12,45 +14,64 @@ def get_client():
|
||||
return client
|
||||
|
||||
|
||||
item_id_not_int = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["path", "item_id"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
@needs_py310
|
||||
def test_post_body_q_bar_content(client: TestClient):
|
||||
response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 5,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
},
|
||||
"q": "bar",
|
||||
}
|
||||
|
||||
|
||||
@needs_py310
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/5?q=bar",
|
||||
{"name": "Foo", "price": 50.5},
|
||||
200,
|
||||
{
|
||||
"item_id": 5,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
},
|
||||
"q": "bar",
|
||||
},
|
||||
),
|
||||
("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}),
|
||||
("/items/5", None, 200, {"item_id": 5}),
|
||||
("/items/foo", None, 422, item_id_not_int),
|
||||
],
|
||||
)
|
||||
def test_post_body(path, body, expected_status, expected_response, client: TestClient):
|
||||
response = client.put(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
def test_post_no_body_q_bar(client: TestClient):
|
||||
response = client.put("/items/5?q=bar", json=None)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": 5, "q": "bar"}
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_no_body(client: TestClient):
|
||||
response = client.put("/items/5", json=None)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": 5}
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_id_foo(client: TestClient):
|
||||
response = client.put("/items/foo", json=None)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["path", "item_id"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "foo",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["path", "item_id"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
@@ -95,7 +116,16 @@ def test_openapi_schema(client: TestClient):
|
||||
},
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Q", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Q",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Q", "type": "string"}
|
||||
),
|
||||
"name": "q",
|
||||
"in": "query",
|
||||
},
|
||||
@@ -103,7 +133,19 @@ def test_openapi_schema(client: TestClient):
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{"$ref": "#/components/schemas/Item"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "Item",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"$ref": "#/components/schemas/Item"}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -118,9 +160,27 @@ def test_openapi_schema(client: TestClient):
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
||||
@@ -1,92 +1,147 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.body_multiple_params.tutorial003 import app
|
||||
|
||||
client = TestClient(app)
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
|
||||
# Test required and embedded body parameters with no bodies sent
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/5",
|
||||
{
|
||||
"importance": 2,
|
||||
"item": {"name": "Foo", "price": 50.5},
|
||||
"user": {"username": "Dave"},
|
||||
},
|
||||
200,
|
||||
{
|
||||
"item_id": 5,
|
||||
"importance": 2,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from docs_src.body_multiple_params.tutorial003 import app
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
def test_post_body_valid(client: TestClient):
|
||||
response = client.put(
|
||||
"/items/5",
|
||||
json={
|
||||
"importance": 2,
|
||||
"item": {"name": "Foo", "price": 50.5},
|
||||
"user": {"username": "Dave"},
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 5,
|
||||
"importance": 2,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
},
|
||||
"user": {"username": "Dave", "full_name": None},
|
||||
}
|
||||
|
||||
|
||||
def test_post_body_no_data(client: TestClient):
|
||||
response = client.put("/items/5", json=None)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "item"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
"user": {"username": "Dave", "full_name": None},
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items/5",
|
||||
None,
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items/5",
|
||||
[],
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_post_body(path, body, expected_status, expected_response):
|
||||
response = client.put(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "user"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
def test_post_body_empty_list(client: TestClient):
|
||||
response = client.put("/items/5", json=[])
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "item"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "user"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
@@ -142,9 +197,27 @@ def test_openapi_schema():
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"User": {
|
||||
@@ -153,7 +226,16 @@ def test_openapi_schema():
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {"title": "Username", "type": "string"},
|
||||
"full_name": {"title": "Full Name", "type": "string"},
|
||||
"full_name": IsDict(
|
||||
{
|
||||
"title": "Full Name",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Full Name", "type": "string"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"Body_update_item_items__item_id__put": {
|
||||
|
||||
@@ -1,92 +1,147 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.body_multiple_params.tutorial003_an import app
|
||||
|
||||
client = TestClient(app)
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
|
||||
# Test required and embedded body parameters with no bodies sent
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/5",
|
||||
{
|
||||
"importance": 2,
|
||||
"item": {"name": "Foo", "price": 50.5},
|
||||
"user": {"username": "Dave"},
|
||||
},
|
||||
200,
|
||||
{
|
||||
"item_id": 5,
|
||||
"importance": 2,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from docs_src.body_multiple_params.tutorial003_an import app
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
def test_post_body_valid(client: TestClient):
|
||||
response = client.put(
|
||||
"/items/5",
|
||||
json={
|
||||
"importance": 2,
|
||||
"item": {"name": "Foo", "price": 50.5},
|
||||
"user": {"username": "Dave"},
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 5,
|
||||
"importance": 2,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
},
|
||||
"user": {"username": "Dave", "full_name": None},
|
||||
}
|
||||
|
||||
|
||||
def test_post_body_no_data(client: TestClient):
|
||||
response = client.put("/items/5", json=None)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "item"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
"user": {"username": "Dave", "full_name": None},
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items/5",
|
||||
None,
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items/5",
|
||||
[],
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_post_body(path, body, expected_status, expected_response):
|
||||
response = client.put(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "user"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
def test_post_body_empty_list(client: TestClient):
|
||||
response = client.put("/items/5", json=[])
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "item"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "user"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
@@ -142,9 +197,27 @@ def test_openapi_schema():
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"User": {
|
||||
@@ -153,7 +226,16 @@ def test_openapi_schema():
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {"title": "Username", "type": "string"},
|
||||
"full_name": {"title": "Full Name", "type": "string"},
|
||||
"full_name": IsDict(
|
||||
{
|
||||
"title": "Full Name",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Full Name", "type": "string"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"Body_update_item_items__item_id__put": {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from ...utils import needs_py310
|
||||
|
||||
@@ -12,85 +14,136 @@ def get_client():
|
||||
return client
|
||||
|
||||
|
||||
# Test required and embedded body parameters with no bodies sent
|
||||
@needs_py310
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/5",
|
||||
{
|
||||
"importance": 2,
|
||||
"item": {"name": "Foo", "price": 50.5},
|
||||
"user": {"username": "Dave"},
|
||||
},
|
||||
200,
|
||||
{
|
||||
"item_id": 5,
|
||||
"importance": 2,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
def test_post_body_valid(client: TestClient):
|
||||
response = client.put(
|
||||
"/items/5",
|
||||
json={
|
||||
"importance": 2,
|
||||
"item": {"name": "Foo", "price": 50.5},
|
||||
"user": {"username": "Dave"},
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 5,
|
||||
"importance": 2,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
},
|
||||
"user": {"username": "Dave", "full_name": None},
|
||||
}
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_body_no_data(client: TestClient):
|
||||
response = client.put("/items/5", json=None)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "item"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
"user": {"username": "Dave", "full_name": None},
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items/5",
|
||||
None,
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items/5",
|
||||
[],
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_post_body(path, body, expected_status, expected_response, client: TestClient):
|
||||
response = client.put(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "user"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_body_empty_list(client: TestClient):
|
||||
response = client.put("/items/5", json=[])
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "item"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "user"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
@@ -150,9 +203,27 @@ def test_openapi_schema(client: TestClient):
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"User": {
|
||||
@@ -161,7 +232,16 @@ def test_openapi_schema(client: TestClient):
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {"title": "Username", "type": "string"},
|
||||
"full_name": {"title": "Full Name", "type": "string"},
|
||||
"full_name": IsDict(
|
||||
{
|
||||
"title": "Full Name",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Full Name", "type": "string"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"Body_update_item_items__item_id__put": {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from ...utils import needs_py39
|
||||
|
||||
@@ -12,85 +14,136 @@ def get_client():
|
||||
return client
|
||||
|
||||
|
||||
# Test required and embedded body parameters with no bodies sent
|
||||
@needs_py39
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/5",
|
||||
{
|
||||
"importance": 2,
|
||||
"item": {"name": "Foo", "price": 50.5},
|
||||
"user": {"username": "Dave"},
|
||||
},
|
||||
200,
|
||||
{
|
||||
"item_id": 5,
|
||||
"importance": 2,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
def test_post_body_valid(client: TestClient):
|
||||
response = client.put(
|
||||
"/items/5",
|
||||
json={
|
||||
"importance": 2,
|
||||
"item": {"name": "Foo", "price": 50.5},
|
||||
"user": {"username": "Dave"},
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 5,
|
||||
"importance": 2,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
},
|
||||
"user": {"username": "Dave", "full_name": None},
|
||||
}
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_post_body_no_data(client: TestClient):
|
||||
response = client.put("/items/5", json=None)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "item"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
"user": {"username": "Dave", "full_name": None},
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items/5",
|
||||
None,
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items/5",
|
||||
[],
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_post_body(path, body, expected_status, expected_response, client: TestClient):
|
||||
response = client.put(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "user"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py39
|
||||
def test_post_body_empty_list(client: TestClient):
|
||||
response = client.put("/items/5", json=[])
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "item"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "user"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py39
|
||||
@@ -150,9 +203,27 @@ def test_openapi_schema(client: TestClient):
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"User": {
|
||||
@@ -161,7 +232,16 @@ def test_openapi_schema(client: TestClient):
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {"title": "Username", "type": "string"},
|
||||
"full_name": {"title": "Full Name", "type": "string"},
|
||||
"full_name": IsDict(
|
||||
{
|
||||
"title": "Full Name",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Full Name", "type": "string"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"Body_update_item_items__item_id__put": {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from ...utils import needs_py310
|
||||
|
||||
@@ -12,85 +14,136 @@ def get_client():
|
||||
return client
|
||||
|
||||
|
||||
# Test required and embedded body parameters with no bodies sent
|
||||
@needs_py310
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items/5",
|
||||
{
|
||||
"importance": 2,
|
||||
"item": {"name": "Foo", "price": 50.5},
|
||||
"user": {"username": "Dave"},
|
||||
},
|
||||
200,
|
||||
{
|
||||
"item_id": 5,
|
||||
"importance": 2,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
def test_post_body_valid(client: TestClient):
|
||||
response = client.put(
|
||||
"/items/5",
|
||||
json={
|
||||
"importance": 2,
|
||||
"item": {"name": "Foo", "price": 50.5},
|
||||
"user": {"username": "Dave"},
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"item_id": 5,
|
||||
"importance": 2,
|
||||
"item": {
|
||||
"name": "Foo",
|
||||
"price": 50.5,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
},
|
||||
"user": {"username": "Dave", "full_name": None},
|
||||
}
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_body_no_data(client: TestClient):
|
||||
response = client.put("/items/5", json=None)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "item"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
"user": {"username": "Dave", "full_name": None},
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items/5",
|
||||
None,
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items/5",
|
||||
[],
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_post_body(path, body, expected_status, expected_response, client: TestClient):
|
||||
response = client.put(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "user"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
def test_post_body_empty_list(client: TestClient):
|
||||
response = client.put("/items/5", json=[])
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "item"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "user"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
"url": match_pydantic_error_url("missing"),
|
||||
},
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "user"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "importance"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py310
|
||||
@@ -150,9 +203,27 @@ def test_openapi_schema(client: TestClient):
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"User": {
|
||||
@@ -161,7 +232,16 @@ def test_openapi_schema(client: TestClient):
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {"title": "Username", "type": "string"},
|
||||
"full_name": {"title": "Full Name", "type": "string"},
|
||||
"full_name": IsDict(
|
||||
{
|
||||
"title": "Full Name",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Full Name", "type": "string"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"Body_update_item_items__item_id__put": {
|
||||
|
||||
@@ -1,33 +1,55 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.body_nested_models.tutorial009 import app
|
||||
|
||||
client = TestClient(app)
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
|
||||
def test_post_body():
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from docs_src.body_nested_models.tutorial009 import app
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
def test_post_body(client: TestClient):
|
||||
data = {"2": 2.2, "3": 3.3}
|
||||
response = client.post("/index-weights/", json=data)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == data
|
||||
|
||||
|
||||
def test_post_invalid_body():
|
||||
def test_post_invalid_body(client: TestClient):
|
||||
data = {"foo": 2.2, "3": 3.3}
|
||||
response = client.post("/index-weights/", json=data)
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "__key__"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["body", "foo", "[key]"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "foo",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "__key__"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from ...utils import needs_py39
|
||||
|
||||
@@ -25,15 +27,30 @@ def test_post_invalid_body(client: TestClient):
|
||||
data = {"foo": 2.2, "3": 3.3}
|
||||
response = client.post("/index-weights/", json=data)
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "__key__"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "int_parsing",
|
||||
"loc": ["body", "foo", "[key]"],
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
||||
"input": "foo",
|
||||
"url": match_pydantic_error_url("int_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "__key__"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_py39
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.body_updates.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
@pytest.fixture(name="client")
|
||||
def get_client():
|
||||
from docs_src.body_updates.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
def test_get():
|
||||
def test_get(client: TestClient):
|
||||
response = client.get("/items/baz")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
@@ -17,7 +23,7 @@ def test_get():
|
||||
}
|
||||
|
||||
|
||||
def test_put():
|
||||
def test_put(client: TestClient):
|
||||
response = client.put(
|
||||
"/items/bar", json={"name": "Barz", "price": 3, "description": None}
|
||||
)
|
||||
@@ -30,7 +36,7 @@ def test_put():
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
def test_openapi_schema(client: TestClient):
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
@@ -118,9 +124,36 @@ def test_openapi_schema():
|
||||
"title": "Item",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"name": IsDict(
|
||||
{
|
||||
"title": "Name",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Name", "type": "string"}
|
||||
),
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"price": IsDict(
|
||||
{
|
||||
"title": "Price",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Price", "type": "number"}
|
||||
),
|
||||
"tax": {"title": "Tax", "type": "number", "default": 10.5},
|
||||
"tags": {
|
||||
"title": "Tags",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from ...utils import needs_py310
|
||||
@@ -128,9 +129,36 @@ def test_openapi_schema(client: TestClient):
|
||||
"title": "Item",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"name": IsDict(
|
||||
{
|
||||
"title": "Name",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Name", "type": "string"}
|
||||
),
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"price": IsDict(
|
||||
{
|
||||
"title": "Price",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Price", "type": "number"}
|
||||
),
|
||||
"tax": {"title": "Tax", "type": "number", "default": 10.5},
|
||||
"tags": {
|
||||
"title": "Tags",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from ...utils import needs_py39
|
||||
@@ -128,9 +129,36 @@ def test_openapi_schema(client: TestClient):
|
||||
"title": "Item",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"name": IsDict(
|
||||
{
|
||||
"title": "Name",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Name", "type": "string"}
|
||||
),
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"price": IsDict(
|
||||
{
|
||||
"title": "Price",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Price", "type": "number"}
|
||||
),
|
||||
"tax": {"title": "Tax", "type": "number", "default": 10.5},
|
||||
"tags": {
|
||||
"title": "Tags",
|
||||
|
||||
@@ -2,13 +2,24 @@ import importlib
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.conditional_openapi import tutorial001
|
||||
from ...utils import needs_pydanticv1
|
||||
|
||||
|
||||
def get_client() -> TestClient:
|
||||
from docs_src.conditional_openapi import tutorial001
|
||||
|
||||
importlib.reload(tutorial001)
|
||||
|
||||
client = TestClient(tutorial001.app)
|
||||
return client
|
||||
|
||||
|
||||
# TODO: pv2 add version with Pydantic v2
|
||||
@needs_pydanticv1
|
||||
def test_disable_openapi(monkeypatch):
|
||||
monkeypatch.setenv("OPENAPI_URL", "")
|
||||
importlib.reload(tutorial001)
|
||||
client = TestClient(tutorial001.app)
|
||||
# Load the client after setting the env var
|
||||
client = get_client()
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 404, response.text
|
||||
response = client.get("/docs")
|
||||
@@ -17,16 +28,19 @@ def test_disable_openapi(monkeypatch):
|
||||
assert response.status_code == 404, response.text
|
||||
|
||||
|
||||
# TODO: pv2 add version with Pydantic v2
|
||||
@needs_pydanticv1
|
||||
def test_root():
|
||||
client = TestClient(tutorial001.app)
|
||||
client = get_client()
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Hello World"}
|
||||
|
||||
|
||||
# TODO: pv2 add version with Pydantic v2
|
||||
@needs_pydanticv1
|
||||
def test_default_openapi():
|
||||
importlib.reload(tutorial001)
|
||||
client = TestClient(tutorial001.app)
|
||||
client = get_client()
|
||||
response = client.get("/docs")
|
||||
assert response.status_code == 200, response.text
|
||||
response = client.get("/redoc")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.cookie_params.tutorial001 import app
|
||||
@@ -56,7 +57,16 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Ads Id", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Ads Id",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Ads Id", "type": "string"}
|
||||
),
|
||||
"name": "ads_id",
|
||||
"in": "cookie",
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.cookie_params.tutorial001_an import app
|
||||
@@ -56,7 +57,16 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Ads Id", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Ads Id",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Ads Id", "type": "string"}
|
||||
),
|
||||
"name": "ads_id",
|
||||
"in": "cookie",
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from ...utils import needs_py310
|
||||
@@ -62,7 +63,16 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Ads Id", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Ads Id",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Ads Id", "type": "string"}
|
||||
),
|
||||
"name": "ads_id",
|
||||
"in": "cookie",
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from ...utils import needs_py39
|
||||
@@ -62,7 +63,16 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Ads Id", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Ads Id",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Ads Id", "type": "string"}
|
||||
),
|
||||
"name": "ads_id",
|
||||
"in": "cookie",
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from ...utils import needs_py310
|
||||
@@ -62,7 +63,16 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Ads Id", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Ads Id",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Ads Id", "type": "string"}
|
||||
),
|
||||
"name": "ads_id",
|
||||
"in": "cookie",
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from docs_src.custom_request_and_route.tutorial002 import app
|
||||
|
||||
@@ -12,16 +14,33 @@ def test_endpoint_works():
|
||||
|
||||
def test_exception_handler_body_access():
|
||||
response = client.post("/", json={"numbers": [1, 2, 3]})
|
||||
|
||||
assert response.json() == {
|
||||
"detail": {
|
||||
"body": '{"numbers": [1, 2, 3]}',
|
||||
"errors": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "value is not a valid list",
|
||||
"type": "type_error.list",
|
||||
}
|
||||
],
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": {
|
||||
"errors": [
|
||||
{
|
||||
"type": "list_type",
|
||||
"loc": ["body"],
|
||||
"msg": "Input should be a valid list",
|
||||
"input": {"numbers": [1, 2, 3]},
|
||||
"url": match_pydantic_error_url("list_type"),
|
||||
}
|
||||
],
|
||||
"body": '{"numbers": [1, 2, 3]}',
|
||||
}
|
||||
}
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": {
|
||||
"body": '{"numbers": [1, 2, 3]}',
|
||||
"errors": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "value is not a valid list",
|
||||
"type": "type_error.list",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
from fastapi.utils import match_pydantic_error_url
|
||||
|
||||
from docs_src.dataclasses.tutorial001 import app
|
||||
|
||||
@@ -19,15 +21,30 @@ def test_post_item():
|
||||
def test_post_invalid_item():
|
||||
response = client.post("/items/", json={"name": "Foo", "price": "invalid price"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "price"],
|
||||
"msg": "value is not a valid float",
|
||||
"type": "type_error.float",
|
||||
}
|
||||
]
|
||||
}
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "float_parsing",
|
||||
"loc": ["body", "price"],
|
||||
"msg": "Input should be a valid number, unable to parse string as an number",
|
||||
"input": "invalid price",
|
||||
"url": match_pydantic_error_url("float_parsing"),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "price"],
|
||||
"msg": "value is not a valid float",
|
||||
"type": "type_error.float",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
@@ -88,8 +105,26 @@ def test_openapi_schema():
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.dataclasses.tutorial002 import app
|
||||
@@ -20,8 +21,7 @@ def test_get_item():
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data == {
|
||||
assert response.json() == {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
@@ -51,13 +51,42 @@ def test_openapi_schema():
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"tags": {
|
||||
"title": "Tags",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tags": IsDict(
|
||||
{
|
||||
"title": "Tags",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"default": [],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "Tags",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
}
|
||||
),
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
"tax": IsDict(
|
||||
{
|
||||
"title": "Tax",
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Tax", "type": "number"}
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.dataclasses.tutorial003 import app
|
||||
@@ -135,11 +136,22 @@ def test_openapi_schema():
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"items": {
|
||||
"title": "Items",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/Item"},
|
||||
},
|
||||
"items": IsDict(
|
||||
{
|
||||
"title": "Items",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/Item"},
|
||||
"default": [],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "Items",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/Item"},
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
@@ -159,7 +171,16 @@ def test_openapi_schema():
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"description": IsDict(
|
||||
{
|
||||
"title": "Description",
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Description", "type": "string"}
|
||||
),
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.dependencies.tutorial001 import app
|
||||
@@ -52,7 +53,16 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Q", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Q",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Q", "type": "string"}
|
||||
),
|
||||
"name": "q",
|
||||
"in": "query",
|
||||
},
|
||||
@@ -102,7 +112,16 @@ def test_openapi_schema():
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Q", "type": "string"},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "Q",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "Q", "type": "string"}
|
||||
),
|
||||
"name": "q",
|
||||
"in": "query",
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user