fastapi 0.99.0__py3-none-any.whl → 0.100.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fastapi might be problematic. Click here for more details.

fastapi/applications.py CHANGED
@@ -15,7 +15,6 @@ from typing import (
15
15
 
16
16
  from fastapi import routing
17
17
  from fastapi.datastructures import Default, DefaultPlaceholder
18
- from fastapi.encoders import DictIntStrAny, SetIntStr
19
18
  from fastapi.exception_handlers import (
20
19
  http_exception_handler,
21
20
  request_validation_exception_handler,
@@ -31,7 +30,7 @@ from fastapi.openapi.docs import (
31
30
  )
32
31
  from fastapi.openapi.utils import get_openapi
33
32
  from fastapi.params import Depends
34
- from fastapi.types import DecoratedCallable
33
+ from fastapi.types import DecoratedCallable, IncEx
35
34
  from fastapi.utils import generate_unique_id
36
35
  from starlette.applications import Starlette
37
36
  from starlette.datastructures import State
@@ -305,8 +304,8 @@ class FastAPI(Starlette):
305
304
  deprecated: Optional[bool] = None,
306
305
  methods: Optional[List[str]] = None,
307
306
  operation_id: Optional[str] = None,
308
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
309
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
307
+ response_model_include: Optional[IncEx] = None,
308
+ response_model_exclude: Optional[IncEx] = None,
310
309
  response_model_by_alias: bool = True,
311
310
  response_model_exclude_unset: bool = False,
312
311
  response_model_exclude_defaults: bool = False,
@@ -363,8 +362,8 @@ class FastAPI(Starlette):
363
362
  deprecated: Optional[bool] = None,
364
363
  methods: Optional[List[str]] = None,
365
364
  operation_id: Optional[str] = None,
366
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
367
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
365
+ response_model_include: Optional[IncEx] = None,
366
+ response_model_exclude: Optional[IncEx] = None,
368
367
  response_model_by_alias: bool = True,
369
368
  response_model_exclude_unset: bool = False,
370
369
  response_model_exclude_defaults: bool = False,
@@ -484,8 +483,8 @@ class FastAPI(Starlette):
484
483
  responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
485
484
  deprecated: Optional[bool] = None,
486
485
  operation_id: Optional[str] = None,
487
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
488
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
486
+ response_model_include: Optional[IncEx] = None,
487
+ response_model_exclude: Optional[IncEx] = None,
489
488
  response_model_by_alias: bool = True,
490
489
  response_model_exclude_unset: bool = False,
491
490
  response_model_exclude_defaults: bool = False,
@@ -539,8 +538,8 @@ class FastAPI(Starlette):
539
538
  responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
540
539
  deprecated: Optional[bool] = None,
541
540
  operation_id: Optional[str] = None,
542
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
543
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
541
+ response_model_include: Optional[IncEx] = None,
542
+ response_model_exclude: Optional[IncEx] = None,
544
543
  response_model_by_alias: bool = True,
545
544
  response_model_exclude_unset: bool = False,
546
545
  response_model_exclude_defaults: bool = False,
@@ -594,8 +593,8 @@ class FastAPI(Starlette):
594
593
  responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
595
594
  deprecated: Optional[bool] = None,
596
595
  operation_id: Optional[str] = None,
597
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
598
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
596
+ response_model_include: Optional[IncEx] = None,
597
+ response_model_exclude: Optional[IncEx] = None,
599
598
  response_model_by_alias: bool = True,
600
599
  response_model_exclude_unset: bool = False,
601
600
  response_model_exclude_defaults: bool = False,
@@ -649,8 +648,8 @@ class FastAPI(Starlette):
649
648
  responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
650
649
  deprecated: Optional[bool] = None,
651
650
  operation_id: Optional[str] = None,
652
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
653
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
651
+ response_model_include: Optional[IncEx] = None,
652
+ response_model_exclude: Optional[IncEx] = None,
654
653
  response_model_by_alias: bool = True,
655
654
  response_model_exclude_unset: bool = False,
656
655
  response_model_exclude_defaults: bool = False,
@@ -704,8 +703,8 @@ class FastAPI(Starlette):
704
703
  responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
705
704
  deprecated: Optional[bool] = None,
706
705
  operation_id: Optional[str] = None,
707
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
708
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
706
+ response_model_include: Optional[IncEx] = None,
707
+ response_model_exclude: Optional[IncEx] = None,
709
708
  response_model_by_alias: bool = True,
710
709
  response_model_exclude_unset: bool = False,
711
710
  response_model_exclude_defaults: bool = False,
@@ -759,8 +758,8 @@ class FastAPI(Starlette):
759
758
  responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
760
759
  deprecated: Optional[bool] = None,
761
760
  operation_id: Optional[str] = None,
762
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
763
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
761
+ response_model_include: Optional[IncEx] = None,
762
+ response_model_exclude: Optional[IncEx] = None,
764
763
  response_model_by_alias: bool = True,
765
764
  response_model_exclude_unset: bool = False,
766
765
  response_model_exclude_defaults: bool = False,
@@ -814,8 +813,8 @@ class FastAPI(Starlette):
814
813
  responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
815
814
  deprecated: Optional[bool] = None,
816
815
  operation_id: Optional[str] = None,
817
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
818
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
816
+ response_model_include: Optional[IncEx] = None,
817
+ response_model_exclude: Optional[IncEx] = None,
819
818
  response_model_by_alias: bool = True,
820
819
  response_model_exclude_unset: bool = False,
821
820
  response_model_exclude_defaults: bool = False,
@@ -869,8 +868,8 @@ class FastAPI(Starlette):
869
868
  responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
870
869
  deprecated: Optional[bool] = None,
871
870
  operation_id: Optional[str] = None,
872
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
873
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
871
+ response_model_include: Optional[IncEx] = None,
872
+ response_model_exclude: Optional[IncEx] = None,
874
873
  response_model_by_alias: bool = True,
875
874
  response_model_exclude_unset: bool = False,
876
875
  response_model_exclude_defaults: bool = False,
fastapi/datastructures.py CHANGED
@@ -1,5 +1,12 @@
1
- from typing import Any, Callable, Dict, Iterable, Type, TypeVar
2
-
1
+ from typing import Any, Callable, Dict, Iterable, Type, TypeVar, cast
2
+
3
+ from fastapi._compat import (
4
+ PYDANTIC_V2,
5
+ CoreSchema,
6
+ GetJsonSchemaHandler,
7
+ JsonSchemaValue,
8
+ general_plain_validator_function,
9
+ )
3
10
  from starlette.datastructures import URL as URL # noqa: F401
4
11
  from starlette.datastructures import Address as Address # noqa: F401
5
12
  from starlette.datastructures import FormData as FormData # noqa: F401
@@ -21,8 +28,28 @@ class UploadFile(StarletteUploadFile):
21
28
  return v
22
29
 
23
30
  @classmethod
24
- def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
25
- field_schema.update({"type": "string", "format": "binary"})
31
+ def _validate(cls, __input_value: Any, _: Any) -> "UploadFile":
32
+ if not isinstance(__input_value, StarletteUploadFile):
33
+ raise ValueError(f"Expected UploadFile, received: {type(__input_value)}")
34
+ return cast(UploadFile, __input_value)
35
+
36
+ if not PYDANTIC_V2:
37
+
38
+ @classmethod
39
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
40
+ field_schema.update({"type": "string", "format": "binary"})
41
+
42
+ @classmethod
43
+ def __get_pydantic_json_schema__(
44
+ cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
45
+ ) -> JsonSchemaValue:
46
+ return {"type": "string", "format": "binary"}
47
+
48
+ @classmethod
49
+ def __get_pydantic_core_schema__(
50
+ cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
51
+ ) -> CoreSchema:
52
+ return general_plain_validator_function(cls._validate)
26
53
 
27
54
 
28
55
  class DefaultPlaceholder:
@@ -1,7 +1,7 @@
1
1
  from typing import Any, Callable, List, Optional, Sequence
2
2
 
3
+ from fastapi._compat import ModelField
3
4
  from fastapi.security.base import SecurityBase
4
- from pydantic.fields import ModelField
5
5
 
6
6
 
7
7
  class SecurityRequirement:
@@ -1,7 +1,6 @@
1
- import dataclasses
2
1
  import inspect
3
2
  from contextlib import contextmanager
4
- from copy import copy, deepcopy
3
+ from copy import deepcopy
5
4
  from typing import (
6
5
  Any,
7
6
  Callable,
@@ -20,6 +19,31 @@ from typing import (
20
19
 
21
20
  import anyio
22
21
  from fastapi import params
22
+ from fastapi._compat import (
23
+ PYDANTIC_V2,
24
+ ErrorWrapper,
25
+ ModelField,
26
+ Required,
27
+ Undefined,
28
+ _regenerate_error_with_loc,
29
+ copy_field_info,
30
+ create_body_model,
31
+ evaluate_forwardref,
32
+ field_annotation_is_scalar,
33
+ get_annotation_from_field_info,
34
+ get_missing_field_error,
35
+ is_bytes_field,
36
+ is_bytes_sequence_field,
37
+ is_scalar_field,
38
+ is_scalar_sequence_field,
39
+ is_sequence_field,
40
+ is_uploadfile_or_nonable_uploadfile_annotation,
41
+ is_uploadfile_sequence_annotation,
42
+ lenient_issubclass,
43
+ sequence_types,
44
+ serialize_sequence_value,
45
+ value_is_sequence,
46
+ )
23
47
  from fastapi.concurrency import (
24
48
  AsyncExitStack,
25
49
  asynccontextmanager,
@@ -31,50 +55,14 @@ from fastapi.security.base import SecurityBase
31
55
  from fastapi.security.oauth2 import OAuth2, SecurityScopes
32
56
  from fastapi.security.open_id_connect_url import OpenIdConnect
33
57
  from fastapi.utils import create_response_field, get_path_param_names
34
- from pydantic import BaseModel, create_model
35
- from pydantic.error_wrappers import ErrorWrapper
36
- from pydantic.errors import MissingError
37
- from pydantic.fields import (
38
- SHAPE_FROZENSET,
39
- SHAPE_LIST,
40
- SHAPE_SEQUENCE,
41
- SHAPE_SET,
42
- SHAPE_SINGLETON,
43
- SHAPE_TUPLE,
44
- SHAPE_TUPLE_ELLIPSIS,
45
- FieldInfo,
46
- ModelField,
47
- Required,
48
- Undefined,
49
- )
50
- from pydantic.schema import get_annotation_from_field_info
51
- from pydantic.typing import evaluate_forwardref, get_args, get_origin
52
- from pydantic.utils import lenient_issubclass
58
+ from pydantic.fields import FieldInfo
53
59
  from starlette.background import BackgroundTasks
54
60
  from starlette.concurrency import run_in_threadpool
55
61
  from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
56
62
  from starlette.requests import HTTPConnection, Request
57
63
  from starlette.responses import Response
58
64
  from starlette.websockets import WebSocket
59
- from typing_extensions import Annotated
60
-
61
- sequence_shapes = {
62
- SHAPE_LIST,
63
- SHAPE_SET,
64
- SHAPE_FROZENSET,
65
- SHAPE_TUPLE,
66
- SHAPE_SEQUENCE,
67
- SHAPE_TUPLE_ELLIPSIS,
68
- }
69
- sequence_types = (list, set, tuple)
70
- sequence_shape_to_type = {
71
- SHAPE_LIST: list,
72
- SHAPE_SET: set,
73
- SHAPE_TUPLE: tuple,
74
- SHAPE_SEQUENCE: list,
75
- SHAPE_TUPLE_ELLIPSIS: list,
76
- }
77
-
65
+ from typing_extensions import Annotated, get_args, get_origin
78
66
 
79
67
  multipart_not_installed_error = (
80
68
  'Form data requires "python-multipart" to be installed. \n'
@@ -216,36 +204,6 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]:
216
204
  )
217
205
 
218
206
 
219
- def is_scalar_field(field: ModelField) -> bool:
220
- field_info = field.field_info
221
- if not (
222
- field.shape == SHAPE_SINGLETON
223
- and not lenient_issubclass(field.type_, BaseModel)
224
- and not lenient_issubclass(field.type_, sequence_types + (dict,))
225
- and not dataclasses.is_dataclass(field.type_)
226
- and not isinstance(field_info, params.Body)
227
- ):
228
- return False
229
- if field.sub_fields:
230
- if not all(is_scalar_field(f) for f in field.sub_fields):
231
- return False
232
- return True
233
-
234
-
235
- def is_scalar_sequence_field(field: ModelField) -> bool:
236
- if (field.shape in sequence_shapes) and not lenient_issubclass(
237
- field.type_, BaseModel
238
- ):
239
- if field.sub_fields is not None:
240
- for sub_field in field.sub_fields:
241
- if not is_scalar_field(sub_field):
242
- return False
243
- return True
244
- if lenient_issubclass(field.type_, sequence_types):
245
- return True
246
- return False
247
-
248
-
249
207
  def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
250
208
  signature = inspect.signature(call)
251
209
  globalns = getattr(call, "__globals__", {})
@@ -364,12 +322,11 @@ def analyze_param(
364
322
  is_path_param: bool,
365
323
  ) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]:
366
324
  field_info = None
367
- used_default_field_info = False
368
325
  depends = None
369
326
  type_annotation: Any = Any
370
327
  if (
371
328
  annotation is not inspect.Signature.empty
372
- and get_origin(annotation) is Annotated # type: ignore[comparison-overlap]
329
+ and get_origin(annotation) is Annotated
373
330
  ):
374
331
  annotated_args = get_args(annotation)
375
332
  type_annotation = annotated_args[0]
@@ -384,7 +341,9 @@ def analyze_param(
384
341
  fastapi_annotation = next(iter(fastapi_annotations), None)
385
342
  if isinstance(fastapi_annotation, FieldInfo):
386
343
  # Copy `field_info` because we mutate `field_info.default` below.
387
- field_info = copy(fastapi_annotation)
344
+ field_info = copy_field_info(
345
+ field_info=fastapi_annotation, annotation=annotation
346
+ )
388
347
  assert field_info.default is Undefined or field_info.default is Required, (
389
348
  f"`{field_info.__class__.__name__}` default value cannot be set in"
390
349
  f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
@@ -415,6 +374,8 @@ def analyze_param(
415
374
  f" together for {param_name!r}"
416
375
  )
417
376
  field_info = value
377
+ if PYDANTIC_V2:
378
+ field_info.annotation = type_annotation
418
379
 
419
380
  if depends is not None and depends.dependency is None:
420
381
  depends.dependency = type_annotation
@@ -433,10 +394,15 @@ def analyze_param(
433
394
  # We might check here that `default_value is Required`, but the fact is that the same
434
395
  # parameter might sometimes be a path parameter and sometimes not. See
435
396
  # `tests/test_infer_param_optionality.py` for an example.
436
- field_info = params.Path()
397
+ field_info = params.Path(annotation=type_annotation)
398
+ elif is_uploadfile_or_nonable_uploadfile_annotation(
399
+ type_annotation
400
+ ) or is_uploadfile_sequence_annotation(type_annotation):
401
+ field_info = params.File(annotation=type_annotation, default=default_value)
402
+ elif not field_annotation_is_scalar(annotation=type_annotation):
403
+ field_info = params.Body(annotation=type_annotation, default=default_value)
437
404
  else:
438
- field_info = params.Query(default=default_value)
439
- used_default_field_info = True
405
+ field_info = params.Query(annotation=type_annotation, default=default_value)
440
406
 
441
407
  field = None
442
408
  if field_info is not None:
@@ -450,8 +416,8 @@ def analyze_param(
450
416
  and getattr(field_info, "in_", None) is None
451
417
  ):
452
418
  field_info.in_ = params.ParamTypes.query
453
- annotation = get_annotation_from_field_info(
454
- annotation if annotation is not inspect.Signature.empty else Any,
419
+ use_annotation = get_annotation_from_field_info(
420
+ type_annotation,
455
421
  field_info,
456
422
  param_name,
457
423
  )
@@ -459,19 +425,15 @@ def analyze_param(
459
425
  alias = param_name.replace("_", "-")
460
426
  else:
461
427
  alias = field_info.alias or param_name
428
+ field_info.alias = alias
462
429
  field = create_response_field(
463
430
  name=param_name,
464
- type_=annotation,
431
+ type_=use_annotation,
465
432
  default=field_info.default,
466
433
  alias=alias,
467
434
  required=field_info.default in (Required, Undefined),
468
435
  field_info=field_info,
469
436
  )
470
- if used_default_field_info:
471
- if lenient_issubclass(field.type_, UploadFile):
472
- field.field_info = params.File(field_info.default)
473
- elif not is_scalar_field(field=field):
474
- field.field_info = params.Body(field_info.default)
475
437
 
476
438
  return type_annotation, depends, field
477
439
 
@@ -554,13 +516,13 @@ async def solve_dependencies(
554
516
  dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
555
517
  ) -> Tuple[
556
518
  Dict[str, Any],
557
- List[ErrorWrapper],
519
+ List[Any],
558
520
  Optional[BackgroundTasks],
559
521
  Response,
560
522
  Dict[Tuple[Callable[..., Any], Tuple[str]], Any],
561
523
  ]:
562
524
  values: Dict[str, Any] = {}
563
- errors: List[ErrorWrapper] = []
525
+ errors: List[Any] = []
564
526
  if response is None:
565
527
  response = Response()
566
528
  del response.headers["content-length"]
@@ -674,7 +636,7 @@ async def solve_dependencies(
674
636
  def request_params_to_args(
675
637
  required_params: Sequence[ModelField],
676
638
  received_params: Union[Mapping[str, Any], QueryParams, Headers],
677
- ) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
639
+ ) -> Tuple[Dict[str, Any], List[Any]]:
678
640
  values = {}
679
641
  errors = []
680
642
  for field in required_params:
@@ -688,23 +650,19 @@ def request_params_to_args(
688
650
  assert isinstance(
689
651
  field_info, params.Param
690
652
  ), "Params must be subclasses of Param"
653
+ loc = (field_info.in_.value, field.alias)
691
654
  if value is None:
692
655
  if field.required:
693
- errors.append(
694
- ErrorWrapper(
695
- MissingError(), loc=(field_info.in_.value, field.alias)
696
- )
697
- )
656
+ errors.append(get_missing_field_error(loc=loc))
698
657
  else:
699
658
  values[field.name] = deepcopy(field.default)
700
659
  continue
701
- v_, errors_ = field.validate(
702
- value, values, loc=(field_info.in_.value, field.alias)
703
- )
660
+ v_, errors_ = field.validate(value, values, loc=loc)
704
661
  if isinstance(errors_, ErrorWrapper):
705
662
  errors.append(errors_)
706
663
  elif isinstance(errors_, list):
707
- errors.extend(errors_)
664
+ new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
665
+ errors.extend(new_errors)
708
666
  else:
709
667
  values[field.name] = v_
710
668
  return values, errors
@@ -713,9 +671,9 @@ def request_params_to_args(
713
671
  async def request_body_to_args(
714
672
  required_params: List[ModelField],
715
673
  received_body: Optional[Union[Dict[str, Any], FormData]],
716
- ) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
674
+ ) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
717
675
  values = {}
718
- errors = []
676
+ errors: List[Dict[str, Any]] = []
719
677
  if required_params:
720
678
  field = required_params[0]
721
679
  field_info = field.field_info
@@ -733,9 +691,7 @@ async def request_body_to_args(
733
691
 
734
692
  value: Optional[Any] = None
735
693
  if received_body is not None:
736
- if (
737
- field.shape in sequence_shapes or field.type_ in sequence_types
738
- ) and isinstance(received_body, FormData):
694
+ if (is_sequence_field(field)) and isinstance(received_body, FormData):
739
695
  value = received_body.getlist(field.alias)
740
696
  else:
741
697
  try:
@@ -748,7 +704,7 @@ async def request_body_to_args(
748
704
  or (isinstance(field_info, params.Form) and value == "")
749
705
  or (
750
706
  isinstance(field_info, params.Form)
751
- and field.shape in sequence_shapes
707
+ and is_sequence_field(field)
752
708
  and len(value) == 0
753
709
  )
754
710
  ):
@@ -759,16 +715,17 @@ async def request_body_to_args(
759
715
  continue
760
716
  if (
761
717
  isinstance(field_info, params.File)
762
- and lenient_issubclass(field.type_, bytes)
718
+ and is_bytes_field(field)
763
719
  and isinstance(value, UploadFile)
764
720
  ):
765
721
  value = await value.read()
766
722
  elif (
767
- field.shape in sequence_shapes
723
+ is_bytes_sequence_field(field)
768
724
  and isinstance(field_info, params.File)
769
- and lenient_issubclass(field.type_, bytes)
770
- and isinstance(value, sequence_types)
725
+ and value_is_sequence(value)
771
726
  ):
727
+ # For types
728
+ assert isinstance(value, sequence_types) # type: ignore[arg-type]
772
729
  results: List[Union[bytes, str]] = []
773
730
 
774
731
  async def process_fn(
@@ -780,24 +737,19 @@ async def request_body_to_args(
780
737
  async with anyio.create_task_group() as tg:
781
738
  for sub_value in value:
782
739
  tg.start_soon(process_fn, sub_value.read)
783
- value = sequence_shape_to_type[field.shape](results)
740
+ value = serialize_sequence_value(field=field, value=results)
784
741
 
785
742
  v_, errors_ = field.validate(value, values, loc=loc)
786
743
 
787
- if isinstance(errors_, ErrorWrapper):
788
- errors.append(errors_)
789
- elif isinstance(errors_, list):
744
+ if isinstance(errors_, list):
790
745
  errors.extend(errors_)
746
+ elif errors_:
747
+ errors.append(errors_)
791
748
  else:
792
749
  values[field.name] = v_
793
750
  return values, errors
794
751
 
795
752
 
796
- def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper:
797
- missing_field_error = ErrorWrapper(MissingError(), loc=loc)
798
- return missing_field_error
799
-
800
-
801
753
  def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
802
754
  flat_dependant = get_flat_dependant(dependant)
803
755
  if not flat_dependant.body_params:
@@ -815,12 +767,16 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
815
767
  for param in flat_dependant.body_params:
816
768
  setattr(param.field_info, "embed", True) # noqa: B010
817
769
  model_name = "Body_" + name
818
- BodyModel: Type[BaseModel] = create_model(model_name)
819
- for f in flat_dependant.body_params:
820
- BodyModel.__fields__[f.name] = f
770
+ BodyModel = create_body_model(
771
+ fields=flat_dependant.body_params, model_name=model_name
772
+ )
821
773
  required = any(True for f in flat_dependant.body_params if f.required)
822
-
823
- BodyFieldInfo_kwargs: Dict[str, Any] = {"default": None}
774
+ BodyFieldInfo_kwargs: Dict[str, Any] = {
775
+ "annotation": BodyModel,
776
+ "alias": "body",
777
+ }
778
+ if not required:
779
+ BodyFieldInfo_kwargs["default"] = None
824
780
  if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params):
825
781
  BodyFieldInfo: Type[params.Body] = params.File
826
782
  elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params):
fastapi/encoders.py CHANGED
@@ -1,15 +1,87 @@
1
1
  import dataclasses
2
+ import datetime
2
3
  from collections import defaultdict, deque
4
+ from decimal import Decimal
3
5
  from enum import Enum
4
- from pathlib import PurePath
6
+ from ipaddress import (
7
+ IPv4Address,
8
+ IPv4Interface,
9
+ IPv4Network,
10
+ IPv6Address,
11
+ IPv6Interface,
12
+ IPv6Network,
13
+ )
14
+ from pathlib import Path, PurePath
15
+ from re import Pattern
5
16
  from types import GeneratorType
6
- from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
17
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
18
+ from uuid import UUID
7
19
 
20
+ from fastapi.types import IncEx
8
21
  from pydantic import BaseModel
9
- from pydantic.json import ENCODERS_BY_TYPE
22
+ from pydantic.color import Color
23
+ from pydantic.networks import NameEmail
24
+ from pydantic.types import SecretBytes, SecretStr
10
25
 
11
- SetIntStr = Set[Union[int, str]]
12
- DictIntStrAny = Dict[Union[int, str], Any]
26
+ from ._compat import PYDANTIC_V2, MultiHostUrl, Url, _model_dump
27
+
28
+
29
+ # Taken from Pydantic v1 as is
30
+ def isoformat(o: Union[datetime.date, datetime.time]) -> str:
31
+ return o.isoformat()
32
+
33
+
34
+ # Taken from Pydantic v1 as is
35
+ # TODO: pv2 should this return strings instead?
36
+ def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
37
+ """
38
+ Encodes a Decimal as int of there's no exponent, otherwise float
39
+
40
+ This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
41
+ where a integer (but not int typed) is used. Encoding this as a float
42
+ results in failed round-tripping between encode and parse.
43
+ Our Id type is a prime example of this.
44
+
45
+ >>> decimal_encoder(Decimal("1.0"))
46
+ 1.0
47
+
48
+ >>> decimal_encoder(Decimal("1"))
49
+ 1
50
+ """
51
+ if dec_value.as_tuple().exponent >= 0: # type: ignore[operator]
52
+ return int(dec_value)
53
+ else:
54
+ return float(dec_value)
55
+
56
+
57
+ ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
58
+ bytes: lambda o: o.decode(),
59
+ Color: str,
60
+ datetime.date: isoformat,
61
+ datetime.datetime: isoformat,
62
+ datetime.time: isoformat,
63
+ datetime.timedelta: lambda td: td.total_seconds(),
64
+ Decimal: decimal_encoder,
65
+ Enum: lambda o: o.value,
66
+ frozenset: list,
67
+ deque: list,
68
+ GeneratorType: list,
69
+ IPv4Address: str,
70
+ IPv4Interface: str,
71
+ IPv4Network: str,
72
+ IPv6Address: str,
73
+ IPv6Interface: str,
74
+ IPv6Network: str,
75
+ NameEmail: str,
76
+ Path: str,
77
+ Pattern: lambda o: o.pattern,
78
+ SecretBytes: str,
79
+ SecretStr: str,
80
+ set: list,
81
+ UUID: str,
82
+ Url: str,
83
+ MultiHostUrl: str,
84
+ }
13
85
 
14
86
 
15
87
  def generate_encoders_by_class_tuples(
@@ -28,8 +100,8 @@ encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)
28
100
 
29
101
  def jsonable_encoder(
30
102
  obj: Any,
31
- include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
32
- exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
103
+ include: Optional[IncEx] = None,
104
+ exclude: Optional[IncEx] = None,
33
105
  by_alias: bool = True,
34
106
  exclude_unset: bool = False,
35
107
  exclude_defaults: bool = False,
@@ -50,10 +122,15 @@ def jsonable_encoder(
50
122
  if exclude is not None and not isinstance(exclude, (set, dict)):
51
123
  exclude = set(exclude)
52
124
  if isinstance(obj, BaseModel):
53
- encoder = getattr(obj.__config__, "json_encoders", {})
54
- if custom_encoder:
55
- encoder.update(custom_encoder)
56
- obj_dict = obj.dict(
125
+ # TODO: remove when deprecating Pydantic v1
126
+ encoders: Dict[Any, Any] = {}
127
+ if not PYDANTIC_V2:
128
+ encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined]
129
+ if custom_encoder:
130
+ encoders.update(custom_encoder)
131
+ obj_dict = _model_dump(
132
+ obj,
133
+ mode="json",
57
134
  include=include,
58
135
  exclude=exclude,
59
136
  by_alias=by_alias,
@@ -67,7 +144,8 @@ def jsonable_encoder(
67
144
  obj_dict,
68
145
  exclude_none=exclude_none,
69
146
  exclude_defaults=exclude_defaults,
70
- custom_encoder=encoder,
147
+ # TODO: remove when deprecating Pydantic v1
148
+ custom_encoder=encoders,
71
149
  sqlalchemy_safe=sqlalchemy_safe,
72
150
  )
73
151
  if dataclasses.is_dataclass(obj):