Browse Source

Add support for Pydantic v1 and above 🎉 (#646)

* Make compatible with pydantic v1

* Remove unused import

* Remove unused ignores

* Update pydantic version

* Fix minor formatting issue

*  Revert removing iterate_in_threadpool

*  Add backwards compatibility with Pydantic 0.32.2

with deprecation warnings

*  Update tests to not break when using Pydantic < 1.0.0

* 📝 Update docs for Pydantic version 1.0.0

* 📌 Update Pydantic range version to support from 0.32.2

* 🎨 Format test imports

*  Add support for Pydantic < 1.2 for populate_validators

*  Add backwards compatibility for Pydantic < 1.2.0 with required fields

* 📌 Relax requirement for Pydantic to < 2.0.0 🎉 🚀

* 💚 Update pragma coverage for older Pydantic versions
pull/742/head
dmontagu 5 years ago
committed by Sebastián Ramírez
parent
commit
ab2b86fe2c
  1. 2
      Pipfile
  2. 6
      docs/src/body_schema/tutorial001.py
  3. 2
      docs/src/body_updates/tutorial002.py
  4. 3
      docs/src/extra_models/tutorial001.py
  5. 3
      docs/src/extra_models/tutorial002.py
  6. 3
      docs/src/response_model/tutorial002.py
  7. 3
      docs/src/response_model/tutorial003.py
  8. 2
      docs/src/response_model/tutorial004.py
  9. 26
      docs/tutorial/body-schema.md
  10. 10
      docs/tutorial/body-updates.md
  11. 4
      docs/tutorial/extra-models.md
  12. 20
      docs/tutorial/response-model.md
  13. 92
      fastapi/applications.py
  14. 3
      fastapi/concurrency.py
  15. 17
      fastapi/dependencies/models.py
  16. 317
      fastapi/dependencies/utils.py
  17. 35
      fastapi/encoders.py
  18. 17
      fastapi/exceptions.py
  19. 74
      fastapi/openapi/models.py
  20. 35
      fastapi/openapi/utils.py
  21. 10
      fastapi/params.py
  22. 206
      fastapi/routing.py
  23. 94
      fastapi/utils.py
  24. 2
      pyproject.toml
  25. 48
      tests/test_application.py
  26. 14
      tests/test_extra_routes.py
  27. 4
      tests/test_infer_param_optionality.py
  28. 10
      tests/test_jsonable_encoder.py
  29. 16
      tests/test_path.py
  30. 2
      tests/test_put_no_body.py
  31. 6
      tests/test_security_oauth2.py
  32. 6
      tests/test_security_oauth2_optional.py
  33. 2
      tests/test_skip_defaults.py
  34. 4
      tests/test_starlette_exception.py
  35. 2
      tests/test_tutorial/test_additional_responses/test_tutorial001.py
  36. 2
      tests/test_tutorial/test_additional_responses/test_tutorial002.py
  37. 2
      tests/test_tutorial/test_additional_responses/test_tutorial003.py
  38. 2
      tests/test_tutorial/test_additional_responses/test_tutorial004.py
  39. 2
      tests/test_tutorial/test_async_sql_databases/test_tutorial001.py
  40. 4
      tests/test_tutorial/test_bigger_applications/test_main.py
  41. 4
      tests/test_tutorial/test_body_multiple_params/test_tutorial003.py
  42. 11
      tests/test_tutorial/test_body_schema/test_tutorial001.py
  43. 4
      tests/test_tutorial/test_body_updates/test_tutorial001.py
  44. 2
      tests/test_tutorial/test_cookie_params/test_tutorial001.py
  45. 2
      tests/test_tutorial/test_events/test_tutorial001.py
  46. 10
      tests/test_tutorial/test_extra_data_types/test_tutorial001.py
  47. 4
      tests/test_tutorial/test_extra_models/test_tutorial003.py
  48. 2
      tests/test_tutorial/test_extra_models/test_tutorial004.py
  49. 2
      tests/test_tutorial/test_extra_models/test_tutorial005.py
  50. 2
      tests/test_tutorial/test_handling_errors/test_tutorial001.py
  51. 2
      tests/test_tutorial/test_handling_errors/test_tutorial002.py
  52. 2
      tests/test_tutorial/test_handling_errors/test_tutorial004.py
  53. 2
      tests/test_tutorial/test_handling_errors/test_tutorial005.py
  54. 2
      tests/test_tutorial/test_path_params/test_tutorial004.py
  55. 2
      tests/test_tutorial/test_path_params/test_tutorial005.py
  56. 2
      tests/test_tutorial/test_query_params/test_tutorial005.py
  57. 2
      tests/test_tutorial/test_query_params/test_tutorial006.py
  58. 2
      tests/test_tutorial/test_query_params/test_tutorial007.py
  59. 4
      tests/test_tutorial/test_response_model/test_tutorial003.py
  60. 2
      tests/test_tutorial/test_response_model/test_tutorial004.py
  61. 4
      tests/test_tutorial/test_response_model/test_tutorial005.py
  62. 4
      tests/test_tutorial/test_response_model/test_tutorial006.py
  63. 6
      tests/test_tutorial/test_security/test_tutorial003.py
  64. 12
      tests/test_tutorial/test_security/test_tutorial005.py
  65. 12
      tests/test_tutorial/test_sql_databases/test_sql_databases.py
  66. 12
      tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py

2
Pipfile

@ -26,7 +26,7 @@ uvicorn = "*"
[packages] [packages]
starlette = "==0.12.9" starlette = "==0.12.9"
pydantic = "==0.32.2" pydantic = "==1.0.0"
databases = {extras = ["sqlite"],version = "*"} databases = {extras = ["sqlite"],version = "*"}
hypercorn = "*" hypercorn = "*"
orjson = "*" orjson = "*"

6
docs/src/body_schema/tutorial001.py

@ -1,13 +1,13 @@
from fastapi import Body, FastAPI from fastapi import Body, FastAPI
from pydantic import BaseModel, Schema from pydantic import BaseModel, Field
app = FastAPI() app = FastAPI()
class Item(BaseModel): class Item(BaseModel):
name: str name: str
description: str = Schema(None, title="The description of the item", max_length=300) description: str = Field(None, title="The description of the item", max_length=300)
price: float = Schema(..., gt=0, description="The price must be greater than zero") price: float = Field(..., gt=0, description="The price must be greater than zero")
tax: float = None tax: float = None

2
docs/src/body_updates/tutorial002.py

@ -31,7 +31,7 @@ async def read_item(item_id: str):
async def update_item(item_id: str, item: Item): async def update_item(item_id: str, item: Item):
stored_item_data = items[item_id] stored_item_data = items[item_id]
stored_item_model = Item(**stored_item_data) stored_item_model = Item(**stored_item_data)
update_data = item.dict(skip_defaults=True) update_data = item.dict(exclude_unset=True)
updated_item = stored_item_model.copy(update=update_data) updated_item = stored_item_model.copy(update=update_data)
items[item_id] = jsonable_encoder(updated_item) items[item_id] = jsonable_encoder(updated_item)
return updated_item return updated_item

3
docs/src/extra_models/tutorial001.py

@ -1,6 +1,5 @@
from fastapi import FastAPI from fastapi import FastAPI
from pydantic import BaseModel from pydantic import BaseModel, EmailStr
from pydantic.types import EmailStr
app = FastAPI() app = FastAPI()

3
docs/src/extra_models/tutorial002.py

@ -1,6 +1,5 @@
from fastapi import FastAPI from fastapi import FastAPI
from pydantic import BaseModel from pydantic import BaseModel, EmailStr
from pydantic.types import EmailStr
app = FastAPI() app = FastAPI()

3
docs/src/response_model/tutorial002.py

@ -1,6 +1,5 @@
from fastapi import FastAPI from fastapi import FastAPI
from pydantic import BaseModel from pydantic import BaseModel, EmailStr
from pydantic.types import EmailStr
app = FastAPI() app = FastAPI()

3
docs/src/response_model/tutorial003.py

@ -1,6 +1,5 @@
from fastapi import FastAPI from fastapi import FastAPI
from pydantic import BaseModel from pydantic import BaseModel, EmailStr
from pydantic.types import EmailStr
app = FastAPI() app = FastAPI()

2
docs/src/response_model/tutorial004.py

@ -21,6 +21,6 @@ items = {
} }
@app.get("/items/{item_id}", response_model=Item, response_model_skip_defaults=True) @app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str): async def read_item(item_id: str):
return items[item_id] return items[item_id]

26
docs/tutorial/body-schema.md

@ -1,6 +1,6 @@
The same way you can declare additional validation and metadata in path operation function parameters with `Query`, `Path` and `Body`, you can declare validation and metadata inside of Pydantic models using `Schema`. The same way you can declare additional validation and metadata in path operation function parameters with `Query`, `Path` and `Body`, you can declare validation and metadata inside of Pydantic models using Pydantic's `Field`.
## Import Schema ## Import `Field`
First, you have to import it: First, you have to import it:
@ -9,32 +9,34 @@ First, you have to import it:
``` ```
!!! warning !!! warning
Notice that `Schema` is imported directly from `pydantic`, not from `fastapi` as are all the rest (`Query`, `Path`, `Body`, etc). Notice that `Field` is imported directly from `pydantic`, not from `fastapi` as are all the rest (`Query`, `Path`, `Body`, etc).
## Declare model attributes ## Declare model attributes
You can then use `Schema` with model attributes: You can then use `Field` with model attributes:
```Python hl_lines="9 10" ```Python hl_lines="9 10"
{!./src/body_schema/tutorial001.py!} {!./src/body_schema/tutorial001.py!}
``` ```
`Schema` works the same way as `Query`, `Path` and `Body`, it has all the same parameters, etc. `Field` works the same way as `Query`, `Path` and `Body`, it has all the same parameters, etc.
!!! note "Technical Details" !!! note "Technical Details"
Actually, `Query`, `Path` and others you'll see next are subclasses of a common `Param` which is itself a subclass of Pydantic's `Schema`. Actually, `Query`, `Path` and others you'll see next create objects of subclasses of a common `Param` class, which is itself a subclass of Pydantic's `FieldInfo` class.
`Body` is also a subclass of `Schema` directly. And there are others you will see later that are subclasses of `Body`. And Pydantic's `Field` returns an instance of `FieldInfo` as well.
But remember that when you import `Query`, `Path` and others from `fastapi`, <a href="https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#recap" target="_blank">those are actually functions that return classes of the same name</a>. `Body` also returns objects of a subclass of `FieldInfo` directly. And there are others you will see later that are subclasses of the `Body` class.
Remember that when you import `Query`, `Path`, and others from `fastapi`, <a href="https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#recap" target="_blank">those are actually functions that return classes of the same name</a>.
!!! tip !!! tip
Notice how each model's attribute with a type, default value and `Schema` has the same structure as a path operation function's parameter, with `Schema` instead of `Path`, `Query` and `Body`. Notice how each model's attribute with a type, default value and `Field` has the same structure as a path operation function's parameter, with `Field` instead of `Path`, `Query` and `Body`.
## Schema extras ## Schema extras
In `Schema`, `Path`, `Query`, `Body` and others you'll see later, you can declare extra parameters apart from those described before. In `Field`, `Path`, `Query`, `Body` and others you'll see later, you can declare extra parameters apart from those described before.
Those parameters will be added as-is to the output JSON Schema. Those parameters will be added as-is to the output JSON Schema.
@ -55,6 +57,6 @@ And it would look in the `/docs` like this:
## Recap ## Recap
You can use Pydantic's `Schema` to declare extra validations and metadata for model attributes. You can use Pydantic's `Field` to declare extra validations and metadata for model attributes.
You can also use the extra keyword arguments to pass additional JSON Schema metadata. You can also use the extra keyword arguments to pass additional JSON Schema metadata.

10
docs/tutorial/body-updates.md

@ -41,15 +41,15 @@ This means that you can send only the data that you want to update, leaving the
But this guide shows you, more or less, how they are intended to be used. But this guide shows you, more or less, how they are intended to be used.
### Using Pydantic's `skip_defaults` parameter ### Using Pydantic's `exclude_unset` parameter
If you want to receive partial updates, it's very useful to use the parameter `skip_defaults` in Pydantic's model's `.dict()`. If you want to receive partial updates, it's very useful to use the parameter `exclude_unset` in Pydantic's model's `.dict()`.
Like `item.dict(skip_defaults=True)`. Like `item.dict(exclude_unset=True)`.
That would generate a `dict` with only the data that was set when creating the `item` model, excluding default values. That would generate a `dict` with only the data that was set when creating the `item` model, excluding default values.
Then you can use this to generate a `dict` with only the data that was set, omitting default values: Then you can use this to generate a `dict` with only the data that was set (sent in the request), omitting default values:
```Python hl_lines="34" ```Python hl_lines="34"
{!./src/body_updates/tutorial002.py!} {!./src/body_updates/tutorial002.py!}
@ -72,7 +72,7 @@ In summary, to apply partial updates you would:
* (Optionally) use `PATCH` instead of `PUT`. * (Optionally) use `PATCH` instead of `PUT`.
* Retrieve the stored data. * Retrieve the stored data.
* Put that data in a Pydantic model. * Put that data in a Pydantic model.
* Generate a `dict` without default values from the input model (using `skip_defaults`). * Generate a `dict` without default values from the input model (using `exclude_unset`).
* This way you can update only the values actually set by the user, instead of overriding values already stored with default values in your model. * This way you can update only the values actually set by the user, instead of overriding values already stored with default values in your model.
* Create a copy of the stored model, updating it's attributes with the received partial updates (using the `update` parameter). * Create a copy of the stored model, updating it's attributes with the received partial updates (using the `update` parameter).
* Convert the copied model to something that can be stored in your DB (for example, using the `jsonable_encoder`). * Convert the copied model to something that can be stored in your DB (for example, using the `jsonable_encoder`).

4
docs/tutorial/extra-models.md

@ -15,7 +15,7 @@ This is especially the case for user models, because:
Here's a general idea of how the models could look like with their password fields and the places where they are used: Here's a general idea of how the models could look like with their password fields and the places where they are used:
```Python hl_lines="8 10 15 21 23 32 34 39 40" ```Python hl_lines="7 9 14 20 22 27 28 31 32 33 38 39"
{!./src/extra_models/tutorial001.py!} {!./src/extra_models/tutorial001.py!}
``` ```
@ -148,7 +148,7 @@ All the data conversion, validation, documentation, etc. will still work as norm
That way, we can declare just the differences between the models (with plaintext `password`, with `hashed_password` and without password): That way, we can declare just the differences between the models (with plaintext `password`, with `hashed_password` and without password):
```Python hl_lines="8 14 15 18 19 22 23" ```Python hl_lines="7 13 14 17 18 21 22"
{!./src/extra_models/tutorial002.py!} {!./src/extra_models/tutorial002.py!}
``` ```

20
docs/tutorial/response-model.md

@ -33,13 +33,13 @@ But most importantly:
Here we are declaring a `UserIn` model, it will contain a plaintext password: Here we are declaring a `UserIn` model, it will contain a plaintext password:
```Python hl_lines="8 10" ```Python hl_lines="7 9"
{!./src/response_model/tutorial002.py!} {!./src/response_model/tutorial002.py!}
``` ```
And we are using this model to declare our input and the same model to declare our output: And we are using this model to declare our input and the same model to declare our output:
```Python hl_lines="16 17" ```Python hl_lines="15 16"
{!./src/response_model/tutorial002.py!} {!./src/response_model/tutorial002.py!}
``` ```
@ -56,19 +56,19 @@ But if we use the same model for another path operation, we could be sending our
We can instead create an input model with the plaintext password and an output model without it: We can instead create an input model with the plaintext password and an output model without it:
```Python hl_lines="8 10 15" ```Python hl_lines="7 9 14"
{!./src/response_model/tutorial003.py!} {!./src/response_model/tutorial003.py!}
``` ```
Here, even though our path operation function is returning the same input user that contains the password: Here, even though our path operation function is returning the same input user that contains the password:
```Python hl_lines="23" ```Python hl_lines="22"
{!./src/response_model/tutorial003.py!} {!./src/response_model/tutorial003.py!}
``` ```
...we declared the `response_model` to be our model `UserOut`, that doesn't include the password: ...we declared the `response_model` to be our model `UserOut`, that doesn't include the password:
```Python hl_lines="21" ```Python hl_lines="20"
{!./src/response_model/tutorial003.py!} {!./src/response_model/tutorial003.py!}
``` ```
@ -100,15 +100,15 @@ but you might want to omit them from the result if they were not actually stored
For example, if you have models with many optional attributes in a NoSQL database, but you don't want to send very long JSON responses full of default values. For example, if you have models with many optional attributes in a NoSQL database, but you don't want to send very long JSON responses full of default values.
### Use the `response_model_skip_defaults` parameter ### Use the `response_model_exclude_unset` parameter
You can set the *path operation decorator* parameter `response_model_skip_defaults=True`: You can set the *path operation decorator* parameter `response_model_exclude_unset=True`:
```Python hl_lines="24" ```Python hl_lines="24"
{!./src/response_model/tutorial004.py!} {!./src/response_model/tutorial004.py!}
``` ```
and those default values won't be included in the response. and those default values won't be included in the response, only the values actually set.
So, if you send a request to that *path operation* for the item with ID `foo`, the response (not including default values) will be: So, if you send a request to that *path operation* for the item with ID `foo`, the response (not including default values) will be:
@ -120,7 +120,7 @@ So, if you send a request to that *path operation* for the item with ID `foo`, t
``` ```
!!! info !!! info
FastAPI uses Pydantic model's `.dict()` with <a href="https://pydantic-docs.helpmanual.io/#copying" target="_blank">its `skip_defaults` parameter</a> to achieve this. FastAPI uses Pydantic model's `.dict()` with <a href="https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeldict" target="_blank">its `exclude_unset` parameter</a> to achieve this.
#### Data with values for fields with defaults #### Data with values for fields with defaults
@ -194,4 +194,4 @@ If you forget to use a `set` and use a `list` or `tuple` instead, FastAPI will s
Use the path operation decorator's parameter `response_model` to define response models and especially to ensure private data is filtered out. Use the path operation decorator's parameter `response_model` to define response models and especially to ensure private data is filtered out.
Use `response_model_skip_defaults` to return only the values explicitly set. Use `response_model_exclude_unset` to return only the values explicitly set.

92
fastapi/applications.py

@ -15,6 +15,7 @@ from fastapi.openapi.docs import (
) )
from fastapi.openapi.utils import get_openapi from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends from fastapi.params import Depends
from fastapi.utils import warning_response_model_skip_defaults_deprecated
from starlette.applications import Starlette from starlette.applications import Starlette
from starlette.datastructures import State from starlette.datastructures import State
from starlette.exceptions import ExceptionMiddleware, HTTPException from starlette.exceptions import ExceptionMiddleware, HTTPException
@ -159,11 +160,14 @@ class FastAPI(Starlette):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> None: ) -> None:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
self.router.add_api_route( self.router.add_api_route(
path, path,
endpoint=endpoint, endpoint=endpoint,
@ -181,7 +185,9 @@ class FastAPI(Starlette):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class, response_class=response_class or self.default_response_class,
name=name, name=name,
@ -205,11 +211,15 @@ class FastAPI(Starlette):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
def decorator(func: Callable) -> Callable: def decorator(func: Callable) -> Callable:
self.router.add_api_route( self.router.add_api_route(
path, path,
@ -228,7 +238,9 @@ class FastAPI(Starlette):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class, response_class=response_class or self.default_response_class,
name=name, name=name,
@ -286,11 +298,14 @@ class FastAPI(Starlette):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.router.get( return self.router.get(
path, path,
response_model=response_model, response_model=response_model,
@ -306,7 +321,9 @@ class FastAPI(Starlette):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class, response_class=response_class or self.default_response_class,
name=name, name=name,
@ -329,11 +346,14 @@ class FastAPI(Starlette):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.router.put( return self.router.put(
path, path,
response_model=response_model, response_model=response_model,
@ -349,7 +369,9 @@ class FastAPI(Starlette):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class, response_class=response_class or self.default_response_class,
name=name, name=name,
@ -372,11 +394,14 @@ class FastAPI(Starlette):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.router.post( return self.router.post(
path, path,
response_model=response_model, response_model=response_model,
@ -392,7 +417,9 @@ class FastAPI(Starlette):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class, response_class=response_class or self.default_response_class,
name=name, name=name,
@ -415,11 +442,14 @@ class FastAPI(Starlette):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.router.delete( return self.router.delete(
path, path,
response_model=response_model, response_model=response_model,
@ -435,7 +465,9 @@ class FastAPI(Starlette):
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
operation_id=operation_id, operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class, response_class=response_class or self.default_response_class,
name=name, name=name,
@ -458,11 +490,14 @@ class FastAPI(Starlette):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.router.options( return self.router.options(
path, path,
response_model=response_model, response_model=response_model,
@ -478,7 +513,9 @@ class FastAPI(Starlette):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class, response_class=response_class or self.default_response_class,
name=name, name=name,
@ -501,11 +538,14 @@ class FastAPI(Starlette):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.router.head( return self.router.head(
path, path,
response_model=response_model, response_model=response_model,
@ -521,7 +561,9 @@ class FastAPI(Starlette):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class, response_class=response_class or self.default_response_class,
name=name, name=name,
@ -544,11 +586,14 @@ class FastAPI(Starlette):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.router.patch( return self.router.patch(
path, path,
response_model=response_model, response_model=response_model,
@ -564,7 +609,9 @@ class FastAPI(Starlette):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class, response_class=response_class or self.default_response_class,
name=name, name=name,
@ -587,11 +634,14 @@ class FastAPI(Starlette):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.router.trace( return self.router.trace(
path, path,
response_model=response_model, response_model=response_model,
@ -607,7 +657,9 @@ class FastAPI(Starlette):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class, response_class=response_class or self.default_response_class,
name=name, name=name,

3
fastapi/concurrency.py

@ -1,6 +1,7 @@
from typing import Any, Callable from typing import Any, Callable
from starlette.concurrency import iterate_in_threadpool, run_in_threadpool # noqa from starlette.concurrency import iterate_in_threadpool # noqa
from starlette.concurrency import run_in_threadpool
asynccontextmanager_error_message = """ asynccontextmanager_error_message = """
FastAPI's contextmanager_in_threadpool require Python 3.7 or above, FastAPI's contextmanager_in_threadpool require Python 3.7 or above,

17
fastapi/dependencies/models.py

@ -1,7 +1,12 @@
from typing import Callable, List, Sequence from typing import Callable, List, Sequence
from fastapi.security.base import SecurityBase from fastapi.security.base import SecurityBase
from pydantic.fields import Field
try:
from pydantic.fields import ModelField
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic.fields import Field as ModelField # type: ignore
param_supported_types = (str, int, float, bool) param_supported_types = (str, int, float, bool)
@ -16,11 +21,11 @@ class Dependant:
def __init__( def __init__(
self, self,
*, *,
path_params: List[Field] = None, path_params: List[ModelField] = None,
query_params: List[Field] = None, query_params: List[ModelField] = None,
header_params: List[Field] = None, header_params: List[ModelField] = None,
cookie_params: List[Field] = None, cookie_params: List[ModelField] = None,
body_params: List[Field] = None, body_params: List[ModelField] = None,
dependencies: List["Dependant"] = None, dependencies: List["Dependant"] = None,
security_schemes: List[SecurityRequirement] = None, security_schemes: List[SecurityRequirement] = None,
name: str = None, name: str = None,

317
fastapi/dependencies/utils.py

@ -27,13 +27,11 @@ from fastapi.dependencies.models import Dependant, SecurityRequirement
from fastapi.security.base import SecurityBase from fastapi.security.base import SecurityBase
from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.oauth2 import OAuth2, SecurityScopes
from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.security.open_id_connect_url import OpenIdConnect
from fastapi.utils import get_path_param_names from fastapi.utils import PYDANTIC_1, get_field_info, get_path_param_names
from pydantic import BaseConfig, BaseModel, Schema, create_model from pydantic import BaseConfig, BaseModel, create_model
from pydantic.error_wrappers import ErrorWrapper from pydantic.error_wrappers import ErrorWrapper
from pydantic.errors import MissingError from pydantic.errors import MissingError
from pydantic.fields import Field, Required, Shape from pydantic.utils import lenient_issubclass
from pydantic.schema import get_annotation_from_schema
from pydantic.utils import ForwardRef, evaluate_forwardref, lenient_issubclass
from starlette.background import BackgroundTasks from starlette.background import BackgroundTasks
from starlette.concurrency import run_in_threadpool from starlette.concurrency import run_in_threadpool
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
@ -41,20 +39,55 @@ from starlette.requests import Request
from starlette.responses import Response from starlette.responses import Response
from starlette.websockets import WebSocket from starlette.websockets import WebSocket
try:
from pydantic.fields import (
SHAPE_LIST,
SHAPE_SEQUENCE,
SHAPE_SET,
SHAPE_SINGLETON,
SHAPE_TUPLE,
SHAPE_TUPLE_ELLIPSIS,
FieldInfo,
ModelField,
Required,
)
from pydantic.schema import get_annotation_from_field_info
from pydantic.typing import ForwardRef, evaluate_forwardref
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic.fields import Field as ModelField # type: ignore
from pydantic.fields import Required, Shape # type: ignore
from pydantic import Schema as FieldInfo # type: ignore
from pydantic.schema import get_annotation_from_schema # type: ignore
from pydantic.utils import ForwardRef, evaluate_forwardref # type: ignore
SHAPE_LIST = Shape.LIST
SHAPE_SEQUENCE = Shape.SEQUENCE
SHAPE_SET = Shape.SET
SHAPE_SINGLETON = Shape.SINGLETON
SHAPE_TUPLE = Shape.TUPLE
SHAPE_TUPLE_ELLIPSIS = Shape.TUPLE_ELLIPS
def get_annotation_from_field_info(
annotation: Any, field_info: FieldInfo, field_name: str
) -> Type[Any]:
return get_annotation_from_schema(annotation, field_info)
sequence_shapes = { sequence_shapes = {
Shape.LIST, SHAPE_LIST,
Shape.SET, SHAPE_SET,
Shape.TUPLE, SHAPE_TUPLE,
Shape.SEQUENCE, SHAPE_SEQUENCE,
Shape.TUPLE_ELLIPS, SHAPE_TUPLE_ELLIPSIS,
} }
sequence_types = (list, set, tuple) sequence_types = (list, set, tuple)
sequence_shape_to_type = { sequence_shape_to_type = {
Shape.LIST: list, SHAPE_LIST: list,
Shape.SET: set, SHAPE_SET: set,
Shape.TUPLE: tuple, SHAPE_TUPLE: tuple,
Shape.SEQUENCE: list, SHAPE_SEQUENCE: list,
Shape.TUPLE_ELLIPS: list, SHAPE_TUPLE_ELLIPSIS: list,
} }
@ -150,12 +183,13 @@ def get_flat_dependant(
return flat_dependant return flat_dependant
def is_scalar_field(field: Field) -> bool: def is_scalar_field(field: ModelField) -> bool:
field_info = get_field_info(field)
if not ( if not (
field.shape == Shape.SINGLETON field.shape == SHAPE_SINGLETON
and not lenient_issubclass(field.type_, BaseModel) and not lenient_issubclass(field.type_, BaseModel)
and not lenient_issubclass(field.type_, sequence_types + (dict,)) and not lenient_issubclass(field.type_, sequence_types + (dict,))
and not isinstance(field.schema, params.Body) and not isinstance(field_info, params.Body)
): ):
return False return False
if field.sub_fields: if field.sub_fields:
@ -164,7 +198,7 @@ def is_scalar_field(field: Field) -> bool:
return True return True
def is_scalar_sequence_field(field: Field) -> bool: def is_scalar_sequence_field(field: ModelField) -> bool:
if (field.shape in sequence_shapes) and not lenient_issubclass( if (field.shape in sequence_shapes) and not lenient_issubclass(
field.type_, BaseModel field.type_, BaseModel
): ):
@ -239,7 +273,9 @@ def get_dependant(
continue continue
if add_non_field_param_to_dependency(param=param, dependant=dependant): if add_non_field_param_to_dependency(param=param, dependant=dependant):
continue continue
param_field = get_param_field(param=param, default_schema=params.Query) param_field = get_param_field(
param=param, default_field_info=params.Query, param_name=param_name
)
if param_name in path_param_names: if param_name in path_param_names:
assert is_scalar_field( assert is_scalar_field(
field=param_field field=param_field
@ -250,7 +286,8 @@ def get_dependant(
ignore_default = True ignore_default = True
param_field = get_param_field( param_field = get_param_field(
param=param, param=param,
default_schema=params.Path, param_name=param_name,
default_field_info=params.Path,
force_type=params.ParamTypes.path, force_type=params.ParamTypes.path,
ignore_default=ignore_default, ignore_default=ignore_default,
) )
@ -262,8 +299,9 @@ def get_dependant(
) and is_scalar_sequence_field(param_field): ) and is_scalar_sequence_field(param_field):
add_param_to_fields(field=param_field, dependant=dependant) add_param_to_fields(field=param_field, dependant=dependant)
else: else:
field_info = get_field_info(param_field)
assert isinstance( assert isinstance(
param_field.schema, params.Body field_info, params.Body
), f"Param: {param_field.name} can only be a request body, using Body(...)" ), f"Param: {param_field.name} can only be a request body, using Body(...)"
dependant.body_params.append(param_field) dependant.body_params.append(param_field)
return dependant return dependant
@ -293,59 +331,82 @@ def add_non_field_param_to_dependency(
def get_param_field( def get_param_field(
*, *,
param: inspect.Parameter, param: inspect.Parameter,
default_schema: Type[params.Param] = params.Param, param_name: str,
default_field_info: Type[params.Param] = params.Param,
force_type: params.ParamTypes = None, force_type: params.ParamTypes = None,
ignore_default: bool = False, ignore_default: bool = False,
) -> Field: ) -> ModelField:
default_value = Required default_value = Required
had_schema = False had_schema = False
if not param.default == param.empty and ignore_default is False: if not param.default == param.empty and ignore_default is False:
default_value = param.default default_value = param.default
if isinstance(default_value, Schema): if isinstance(default_value, FieldInfo):
had_schema = True had_schema = True
schema = default_value field_info = default_value
default_value = schema.default default_value = field_info.default
if isinstance(schema, params.Param) and getattr(schema, "in_", None) is None: if (
schema.in_ = default_schema.in_ isinstance(field_info, params.Param)
and getattr(field_info, "in_", None) is None
):
field_info.in_ = default_field_info.in_
if force_type: if force_type:
schema.in_ = force_type # type: ignore field_info.in_ = force_type # type: ignore
else: else:
schema = default_schema(default_value) field_info = default_field_info(default_value)
required = default_value == Required required = default_value == Required
annotation: Any = Any annotation: Any = Any
if not param.annotation == param.empty: if not param.annotation == param.empty:
annotation = param.annotation annotation = param.annotation
annotation = get_annotation_from_schema(annotation, schema) annotation = get_annotation_from_field_info(annotation, field_info, param_name)
if not schema.alias and getattr(schema, "convert_underscores", None): if not field_info.alias and getattr(field_info, "convert_underscores", None):
alias = param.name.replace("_", "-") alias = param.name.replace("_", "-")
else: else:
alias = schema.alias or param.name alias = field_info.alias or param.name
field = Field( if PYDANTIC_1:
name=param.name, field = ModelField(
type_=annotation, name=param.name,
default=None if required else default_value, type_=annotation,
alias=alias, default=None if required else default_value,
required=required, alias=alias,
model_config=BaseConfig, required=required,
class_validators={}, model_config=BaseConfig,
schema=schema, class_validators={},
) field_info=field_info,
)
# TODO: remove when removing support for Pydantic < 1.2.0
field.required = required
else: # pragma: nocover
field = ModelField( # type: ignore
name=param.name,
type_=annotation,
default=None if required else default_value,
alias=alias,
required=required,
model_config=BaseConfig,
class_validators={},
schema=field_info,
)
field.required = required
if not had_schema and not is_scalar_field(field=field): if not had_schema and not is_scalar_field(field=field):
field.schema = params.Body(schema.default) if PYDANTIC_1:
field.field_info = params.Body(field_info.default)
else:
field.schema = params.Body(field_info.default) # type: ignore # pragma: nocover
return field return field
def add_param_to_fields(*, field: Field, dependant: Dependant) -> None: def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None:
field.schema = cast(params.Param, field.schema) field_info = cast(params.Param, get_field_info(field))
if field.schema.in_ == params.ParamTypes.path: if field_info.in_ == params.ParamTypes.path:
dependant.path_params.append(field) dependant.path_params.append(field)
elif field.schema.in_ == params.ParamTypes.query: elif field_info.in_ == params.ParamTypes.query:
dependant.query_params.append(field) dependant.query_params.append(field)
elif field.schema.in_ == params.ParamTypes.header: elif field_info.in_ == params.ParamTypes.header:
dependant.header_params.append(field) dependant.header_params.append(field)
else: else:
assert ( assert (
field.schema.in_ == params.ParamTypes.cookie field_info.in_ == params.ParamTypes.cookie
), f"non-body parameters must be in path, query, header or cookie: {field.name}" ), f"non-body parameters must be in path, query, header or cookie: {field.name}"
dependant.cookie_params.append(field) dependant.cookie_params.append(field)
@ -506,7 +567,7 @@ async def solve_dependencies(
def request_params_to_args( def request_params_to_args(
required_params: Sequence[Field], required_params: Sequence[ModelField],
received_params: Union[Mapping[str, Any], QueryParams, Headers], received_params: Union[Mapping[str, Any], QueryParams, Headers],
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]: ) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
values = {} values = {}
@ -518,21 +579,32 @@ def request_params_to_args(
value = received_params.getlist(field.alias) or field.default value = received_params.getlist(field.alias) or field.default
else: else:
value = received_params.get(field.alias) value = received_params.get(field.alias)
schema = field.schema field_info = get_field_info(field)
assert isinstance(schema, params.Param), "Params must be subclasses of Param" assert isinstance(
field_info, params.Param
), "Params must be subclasses of Param"
if value is None: if value is None:
if field.required: if field.required:
errors.append( if PYDANTIC_1:
ErrorWrapper( errors.append(
MissingError(), ErrorWrapper(
loc=(schema.in_.value, field.alias), MissingError(), loc=(field_info.in_.value, field.alias)
config=BaseConfig, )
)
else: # pragma: nocover
errors.append(
ErrorWrapper( # type: ignore
MissingError(),
loc=(field_info.in_.value, field.alias),
config=BaseConfig,
)
) )
)
else: else:
values[field.name] = deepcopy(field.default) values[field.name] = deepcopy(field.default)
continue continue
v_, errors_ = field.validate(value, values, loc=(schema.in_.value, field.alias)) v_, errors_ = field.validate(
value, values, loc=(field_info.in_.value, field.alias)
)
if isinstance(errors_, ErrorWrapper): if isinstance(errors_, ErrorWrapper):
errors.append(errors_) errors.append(errors_)
elif isinstance(errors_, list): elif isinstance(errors_, list):
@ -543,14 +615,15 @@ def request_params_to_args(
async def request_body_to_args( async def request_body_to_args(
required_params: List[Field], required_params: List[ModelField],
received_body: Optional[Union[Dict[str, Any], FormData]], received_body: Optional[Union[Dict[str, Any], FormData]],
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]: ) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
values = {} values = {}
errors = [] errors = []
if required_params: if required_params:
field = required_params[0] field = required_params[0]
embed = getattr(field.schema, "embed", None) field_info = get_field_info(field)
embed = getattr(field_info, "embed", None)
if len(required_params) == 1 and not embed: if len(required_params) == 1 and not embed:
received_body = {field.alias: received_body} received_body = {field.alias: received_body}
for field in required_params: for field in required_params:
@ -564,31 +637,38 @@ async def request_body_to_args(
value = received_body.get(field.alias) value = received_body.get(field.alias)
if ( if (
value is None value is None
or (isinstance(field.schema, params.Form) and value == "") or (isinstance(field_info, params.Form) and value == "")
or ( or (
isinstance(field.schema, params.Form) isinstance(field_info, params.Form)
and field.shape in sequence_shapes and field.shape in sequence_shapes
and len(value) == 0 and len(value) == 0
) )
): ):
if field.required: if field.required:
errors.append( if PYDANTIC_1:
ErrorWrapper( errors.append(
MissingError(), loc=("body", field.alias), config=BaseConfig ErrorWrapper(MissingError(), loc=("body", field.alias))
)
else: # pragma: nocover
errors.append(
ErrorWrapper( # type: ignore
MissingError(),
loc=("body", field.alias),
config=BaseConfig,
)
) )
)
else: else:
values[field.name] = deepcopy(field.default) values[field.name] = deepcopy(field.default)
continue continue
if ( if (
isinstance(field.schema, params.File) isinstance(field_info, params.File)
and lenient_issubclass(field.type_, bytes) and lenient_issubclass(field.type_, bytes)
and isinstance(value, UploadFile) and isinstance(value, UploadFile)
): ):
value = await value.read() value = await value.read()
elif ( elif (
field.shape in sequence_shapes field.shape in sequence_shapes
and isinstance(field.schema, params.File) and isinstance(field_info, params.File)
and lenient_issubclass(field.type_, bytes) and lenient_issubclass(field.type_, bytes)
and isinstance(value, sequence_types) and isinstance(value, sequence_types)
): ):
@ -605,31 +685,45 @@ async def request_body_to_args(
return values, errors return values, errors
def get_schema_compatible_field(*, field: Field) -> Field: def get_schema_compatible_field(*, field: ModelField) -> ModelField:
out_field = field out_field = field
if lenient_issubclass(field.type_, UploadFile): if lenient_issubclass(field.type_, UploadFile):
use_type: type = bytes use_type: type = bytes
if field.shape in sequence_shapes: if field.shape in sequence_shapes:
use_type = List[bytes] use_type = List[bytes]
out_field = Field( if PYDANTIC_1:
name=field.name, out_field = ModelField(
type_=use_type, name=field.name,
class_validators=field.class_validators, type_=use_type,
model_config=field.model_config, class_validators=field.class_validators,
default=field.default, model_config=field.model_config,
required=field.required, default=field.default,
alias=field.alias, required=field.required,
schema=field.schema, alias=field.alias,
) field_info=field.field_info,
)
else: # pragma: nocover
out_field = ModelField( # type: ignore
name=field.name,
type_=use_type,
class_validators=field.class_validators,
model_config=field.model_config,
default=field.default,
required=field.required,
alias=field.alias,
schema=field.schema, # type: ignore
)
return out_field return out_field
def get_body_field(*, dependant: Dependant, name: str) -> Optional[Field]: def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
flat_dependant = get_flat_dependant(dependant) flat_dependant = get_flat_dependant(dependant)
if not flat_dependant.body_params: if not flat_dependant.body_params:
return None return None
first_param = flat_dependant.body_params[0] first_param = flat_dependant.body_params[0]
embed = getattr(first_param.schema, "embed", None) field_info = get_field_info(first_param)
embed = getattr(field_info, "embed", None)
if len(flat_dependant.body_params) == 1 and not embed: if len(flat_dependant.body_params) == 1 and not embed:
return get_schema_compatible_field(field=first_param) return get_schema_compatible_field(field=first_param)
model_name = "Body_" + name model_name = "Body_" + name
@ -638,30 +732,45 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[Field]:
BodyModel.__fields__[f.name] = get_schema_compatible_field(field=f) BodyModel.__fields__[f.name] = get_schema_compatible_field(field=f)
required = any(True for f in flat_dependant.body_params if f.required) required = any(True for f in flat_dependant.body_params if f.required)
BodySchema_kwargs: Dict[str, Any] = dict(default=None) BodyFieldInfo_kwargs: Dict[str, Any] = dict(default=None)
if any(isinstance(f.schema, params.File) for f in flat_dependant.body_params): if any(
BodySchema: Type[params.Body] = params.File isinstance(get_field_info(f), params.File) for f in flat_dependant.body_params
elif any(isinstance(f.schema, params.Form) for f in flat_dependant.body_params): ):
BodySchema = params.Form BodyFieldInfo: Type[params.Body] = params.File
elif any(
isinstance(get_field_info(f), params.Form) for f in flat_dependant.body_params
):
BodyFieldInfo = params.Form
else: else:
BodySchema = params.Body BodyFieldInfo = params.Body
body_param_media_types = [ body_param_media_types = [
getattr(f.schema, "media_type") getattr(get_field_info(f), "media_type")
for f in flat_dependant.body_params for f in flat_dependant.body_params
if isinstance(f.schema, params.Body) if isinstance(get_field_info(f), params.Body)
] ]
if len(set(body_param_media_types)) == 1: if len(set(body_param_media_types)) == 1:
BodySchema_kwargs["media_type"] = body_param_media_types[0] BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0]
if PYDANTIC_1:
field = Field( field = ModelField(
name="body", name="body",
type_=BodyModel, type_=BodyModel,
default=None, default=None,
required=required, required=required,
model_config=BaseConfig, model_config=BaseConfig,
class_validators={}, class_validators={},
alias="body", alias="body",
schema=BodySchema(**BodySchema_kwargs), field_info=BodyFieldInfo(**BodyFieldInfo_kwargs),
) )
else: # pragma: nocover
field = ModelField( # type: ignore
name="body",
type_=BodyModel,
default=None,
required=required,
model_config=BaseConfig,
class_validators={},
alias="body",
schema=BodyFieldInfo(**BodyFieldInfo_kwargs),
)
return field return field

35
fastapi/encoders.py

@ -2,6 +2,7 @@ from enum import Enum
from types import GeneratorType from types import GeneratorType
from typing import Any, Dict, List, Set, Union from typing import Any, Dict, List, Set, Union
from fastapi.utils import PYDANTIC_1, logger
from pydantic import BaseModel from pydantic import BaseModel
from pydantic.json import ENCODERS_BY_TYPE from pydantic.json import ENCODERS_BY_TYPE
@ -14,24 +15,40 @@ def jsonable_encoder(
include: Union[SetIntStr, DictIntStrAny] = None, include: Union[SetIntStr, DictIntStrAny] = None,
exclude: Union[SetIntStr, DictIntStrAny] = set(), exclude: Union[SetIntStr, DictIntStrAny] = set(),
by_alias: bool = True, by_alias: bool = True,
skip_defaults: bool = False, skip_defaults: bool = None,
exclude_unset: bool = False,
include_none: bool = True, include_none: bool = True,
custom_encoder: dict = {}, custom_encoder: dict = {},
sqlalchemy_safe: bool = True, sqlalchemy_safe: bool = True,
) -> Any: ) -> Any:
if skip_defaults is not None:
logger.warning( # pragma: nocover
"skip_defaults in jsonable_encoder has been deprecated in \
favor of exclude_unset to keep in line with Pydantic v1, support for it \
will be removed soon."
)
if include is not None and not isinstance(include, set): if include is not None and not isinstance(include, set):
include = set(include) include = set(include)
if exclude is not None and not isinstance(exclude, set): if exclude is not None and not isinstance(exclude, set):
exclude = set(exclude) exclude = set(exclude)
if isinstance(obj, BaseModel): if isinstance(obj, BaseModel):
encoder = getattr(obj.Config, "json_encoders", custom_encoder) encoder = getattr(obj.Config, "json_encoders", custom_encoder)
return jsonable_encoder( if PYDANTIC_1:
obj.dict( obj_dict = obj.dict(
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=bool(exclude_unset or skip_defaults),
)
else: # pragma: nocover
obj_dict = obj.dict(
include=include, include=include,
exclude=exclude, exclude=exclude,
by_alias=by_alias, by_alias=by_alias,
skip_defaults=skip_defaults, skip_defaults=bool(exclude_unset or skip_defaults),
), )
return jsonable_encoder(
obj_dict,
include_none=include_none, include_none=include_none,
custom_encoder=encoder, custom_encoder=encoder,
sqlalchemy_safe=sqlalchemy_safe, sqlalchemy_safe=sqlalchemy_safe,
@ -55,7 +72,7 @@ def jsonable_encoder(
encoded_key = jsonable_encoder( encoded_key = jsonable_encoder(
key, key,
by_alias=by_alias, by_alias=by_alias,
skip_defaults=skip_defaults, exclude_unset=exclude_unset,
include_none=include_none, include_none=include_none,
custom_encoder=custom_encoder, custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe, sqlalchemy_safe=sqlalchemy_safe,
@ -63,7 +80,7 @@ def jsonable_encoder(
encoded_value = jsonable_encoder( encoded_value = jsonable_encoder(
value, value,
by_alias=by_alias, by_alias=by_alias,
skip_defaults=skip_defaults, exclude_unset=exclude_unset,
include_none=include_none, include_none=include_none,
custom_encoder=custom_encoder, custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe, sqlalchemy_safe=sqlalchemy_safe,
@ -79,7 +96,7 @@ def jsonable_encoder(
include=include, include=include,
exclude=exclude, exclude=exclude,
by_alias=by_alias, by_alias=by_alias,
skip_defaults=skip_defaults, exclude_unset=exclude_unset,
include_none=include_none, include_none=include_none,
custom_encoder=custom_encoder, custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe, sqlalchemy_safe=sqlalchemy_safe,
@ -107,7 +124,7 @@ def jsonable_encoder(
return jsonable_encoder( return jsonable_encoder(
data, data,
by_alias=by_alias, by_alias=by_alias,
skip_defaults=skip_defaults, exclude_unset=exclude_unset,
include_none=include_none, include_none=include_none,
custom_encoder=custom_encoder, custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe, sqlalchemy_safe=sqlalchemy_safe,

17
fastapi/exceptions.py

@ -1,6 +1,7 @@
from typing import Any, Sequence from typing import Any, Sequence
from pydantic import ValidationError from fastapi.utils import PYDANTIC_1
from pydantic import ValidationError, create_model
from pydantic.error_wrappers import ErrorList from pydantic.error_wrappers import ErrorList
from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.requests import Request from starlette.requests import Request
@ -15,11 +16,21 @@ class HTTPException(StarletteHTTPException):
self.headers = headers self.headers = headers
RequestErrorModel = create_model("Request")
WebSocketErrorModel = create_model("WebSocket")
class RequestValidationError(ValidationError): class RequestValidationError(ValidationError):
def __init__(self, errors: Sequence[ErrorList]) -> None: def __init__(self, errors: Sequence[ErrorList]) -> None:
super().__init__(errors, Request) if PYDANTIC_1:
super().__init__(errors, RequestErrorModel)
else:
super().__init__(errors, Request) # type: ignore # pragma: nocover
class WebSocketRequestValidationError(ValidationError): class WebSocketRequestValidationError(ValidationError):
def __init__(self, errors: Sequence[ErrorList]) -> None: def __init__(self, errors: Sequence[ErrorList]) -> None:
super().__init__(errors, WebSocket) if PYDANTIC_1:
super().__init__(errors, WebSocketErrorModel)
else:
super().__init__(errors, WebSocket) # type: ignore # pragma: nocover

74
fastapi/openapi/models.py

@ -1,17 +1,25 @@
import logging
from enum import Enum from enum import Enum
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel, Schema as PSchema from fastapi.utils import logger
from pydantic.types import UrlStr from pydantic import BaseModel
logger = logging.getLogger("fastapi") try:
from pydantic import AnyUrl, Field
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic import Schema as Field # type: ignore
from pydantic import UrlStr as AnyUrl # type: ignore
try: try:
import email_validator import email_validator
assert email_validator # make autoflake ignore the unused import assert email_validator # make autoflake ignore the unused import
from pydantic.types import EmailStr try:
from pydantic import EmailStr
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic.types import EmailStr # type: ignore
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
logger.warning( logger.warning(
"email-validator not installed, email fields will be treated as str.\n" "email-validator not installed, email fields will be treated as str.\n"
@ -24,13 +32,13 @@ except ImportError: # pragma: no cover
class Contact(BaseModel): class Contact(BaseModel):
name: Optional[str] = None name: Optional[str] = None
url: Optional[UrlStr] = None url: Optional[AnyUrl] = None
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
class License(BaseModel): class License(BaseModel):
name: str name: str
url: Optional[UrlStr] = None url: Optional[AnyUrl] = None
class Info(BaseModel): class Info(BaseModel):
@ -49,13 +57,13 @@ class ServerVariable(BaseModel):
class Server(BaseModel): class Server(BaseModel):
url: UrlStr url: AnyUrl
description: Optional[str] = None description: Optional[str] = None
variables: Optional[Dict[str, ServerVariable]] = None variables: Optional[Dict[str, ServerVariable]] = None
class Reference(BaseModel): class Reference(BaseModel):
ref: str = PSchema(..., alias="$ref") # type: ignore ref: str = Field(..., alias="$ref")
class Discriminator(BaseModel): class Discriminator(BaseModel):
@ -73,32 +81,32 @@ class XML(BaseModel):
class ExternalDocumentation(BaseModel): class ExternalDocumentation(BaseModel):
description: Optional[str] = None description: Optional[str] = None
url: UrlStr url: AnyUrl
class SchemaBase(BaseModel): class SchemaBase(BaseModel):
ref: Optional[str] = PSchema(None, alias="$ref") # type: ignore ref: Optional[str] = Field(None, alias="$ref")
title: Optional[str] = None title: Optional[str] = None
multipleOf: Optional[float] = None multipleOf: Optional[float] = None
maximum: Optional[float] = None maximum: Optional[float] = None
exclusiveMaximum: Optional[float] = None exclusiveMaximum: Optional[float] = None
minimum: Optional[float] = None minimum: Optional[float] = None
exclusiveMinimum: Optional[float] = None exclusiveMinimum: Optional[float] = None
maxLength: Optional[int] = PSchema(None, gte=0) # type: ignore maxLength: Optional[int] = Field(None, gte=0)
minLength: Optional[int] = PSchema(None, gte=0) # type: ignore minLength: Optional[int] = Field(None, gte=0)
pattern: Optional[str] = None pattern: Optional[str] = None
maxItems: Optional[int] = PSchema(None, gte=0) # type: ignore maxItems: Optional[int] = Field(None, gte=0)
minItems: Optional[int] = PSchema(None, gte=0) # type: ignore minItems: Optional[int] = Field(None, gte=0)
uniqueItems: Optional[bool] = None uniqueItems: Optional[bool] = None
maxProperties: Optional[int] = PSchema(None, gte=0) # type: ignore maxProperties: Optional[int] = Field(None, gte=0)
minProperties: Optional[int] = PSchema(None, gte=0) # type: ignore minProperties: Optional[int] = Field(None, gte=0)
required: Optional[List[str]] = None required: Optional[List[str]] = None
enum: Optional[List[str]] = None enum: Optional[List[str]] = None
type: Optional[str] = None type: Optional[str] = None
allOf: Optional[List[Any]] = None allOf: Optional[List[Any]] = None
oneOf: Optional[List[Any]] = None oneOf: Optional[List[Any]] = None
anyOf: Optional[List[Any]] = None anyOf: Optional[List[Any]] = None
not_: Optional[List[Any]] = PSchema(None, alias="not") # type: ignore not_: Optional[List[Any]] = Field(None, alias="not")
items: Optional[Any] = None items: Optional[Any] = None
properties: Optional[Dict[str, Any]] = None properties: Optional[Dict[str, Any]] = None
additionalProperties: Optional[Union[Dict[str, Any], bool]] = None additionalProperties: Optional[Union[Dict[str, Any], bool]] = None
@ -119,17 +127,17 @@ class Schema(SchemaBase):
allOf: Optional[List[SchemaBase]] = None allOf: Optional[List[SchemaBase]] = None
oneOf: Optional[List[SchemaBase]] = None oneOf: Optional[List[SchemaBase]] = None
anyOf: Optional[List[SchemaBase]] = None anyOf: Optional[List[SchemaBase]] = None
not_: Optional[List[SchemaBase]] = PSchema(None, alias="not") # type: ignore not_: Optional[List[SchemaBase]] = Field(None, alias="not")
items: Optional[SchemaBase] = None items: Optional[SchemaBase] = None
properties: Optional[Dict[str, SchemaBase]] = None properties: Optional[Dict[str, SchemaBase]] = None
additionalProperties: Optional[Union[SchemaBase, bool]] = None # type: ignore additionalProperties: Optional[Union[Dict[str, Any], bool]] = None
class Example(BaseModel): class Example(BaseModel):
summary: Optional[str] = None summary: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
value: Optional[Any] = None value: Optional[Any] = None
externalValue: Optional[UrlStr] = None externalValue: Optional[AnyUrl] = None
class ParameterInType(Enum): class ParameterInType(Enum):
@ -149,9 +157,7 @@ class Encoding(BaseModel):
class MediaType(BaseModel): class MediaType(BaseModel):
schema_: Optional[Union[Schema, Reference]] = PSchema( # type: ignore schema_: Optional[Union[Schema, Reference]] = Field(None, alias="schema")
None, alias="schema"
)
example: Optional[Any] = None example: Optional[Any] = None
examples: Optional[Dict[str, Union[Example, Reference]]] = None examples: Optional[Dict[str, Union[Example, Reference]]] = None
encoding: Optional[Dict[str, Encoding]] = None encoding: Optional[Dict[str, Encoding]] = None
@ -165,9 +171,7 @@ class ParameterBase(BaseModel):
style: Optional[str] = None style: Optional[str] = None
explode: Optional[bool] = None explode: Optional[bool] = None
allowReserved: Optional[bool] = None allowReserved: Optional[bool] = None
schema_: Optional[Union[Schema, Reference]] = PSchema( # type: ignore schema_: Optional[Union[Schema, Reference]] = Field(None, alias="schema")
None, alias="schema"
)
example: Optional[Any] = None example: Optional[Any] = None
examples: Optional[Dict[str, Union[Example, Reference]]] = None examples: Optional[Dict[str, Union[Example, Reference]]] = None
# Serialization rules for more complex scenarios # Serialization rules for more complex scenarios
@ -176,7 +180,7 @@ class ParameterBase(BaseModel):
class Parameter(ParameterBase): class Parameter(ParameterBase):
name: str name: str
in_: ParameterInType = PSchema(..., alias="in") # type: ignore in_: ParameterInType = Field(..., alias="in")
class Header(ParameterBase): class Header(ParameterBase):
@ -227,7 +231,7 @@ class Operation(BaseModel):
class PathItem(BaseModel): class PathItem(BaseModel):
ref: Optional[str] = PSchema(None, alias="$ref") # type: ignore ref: Optional[str] = Field(None, alias="$ref")
summary: Optional[str] = None summary: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
get: Optional[Operation] = None get: Optional[Operation] = None
@ -255,7 +259,7 @@ class SecuritySchemeType(Enum):
class SecurityBase(BaseModel): class SecurityBase(BaseModel):
type_: SecuritySchemeType = PSchema(..., alias="type") # type: ignore type_: SecuritySchemeType = Field(..., alias="type")
description: Optional[str] = None description: Optional[str] = None
@ -266,13 +270,13 @@ class APIKeyIn(Enum):
class APIKey(SecurityBase): class APIKey(SecurityBase):
type_ = PSchema(SecuritySchemeType.apiKey, alias="type") # type: ignore type_ = Field(SecuritySchemeType.apiKey, alias="type")
in_: APIKeyIn = PSchema(..., alias="in") # type: ignore in_: APIKeyIn = Field(..., alias="in")
name: str name: str
class HTTPBase(SecurityBase): class HTTPBase(SecurityBase):
type_ = PSchema(SecuritySchemeType.http, alias="type") # type: ignore type_ = Field(SecuritySchemeType.http, alias="type")
scheme: str scheme: str
@ -311,12 +315,12 @@ class OAuthFlows(BaseModel):
class OAuth2(SecurityBase): class OAuth2(SecurityBase):
type_ = PSchema(SecuritySchemeType.oauth2, alias="type") # type: ignore type_ = Field(SecuritySchemeType.oauth2, alias="type")
flows: OAuthFlows flows: OAuthFlows
class OpenIdConnect(SecurityBase): class OpenIdConnect(SecurityBase):
type_ = PSchema(SecuritySchemeType.openIdConnect, alias="type") # type: ignore type_ = Field(SecuritySchemeType.openIdConnect, alias="type")
openIdConnectUrl: str openIdConnectUrl: str

35
fastapi/openapi/utils.py

@ -14,16 +14,23 @@ from fastapi.openapi.models import OpenAPI
from fastapi.params import Body, Param from fastapi.params import Body, Param
from fastapi.utils import ( from fastapi.utils import (
generate_operation_id_for_path, generate_operation_id_for_path,
get_field_info,
get_flat_models_from_routes, get_flat_models_from_routes,
get_model_definitions, get_model_definitions,
) )
from pydantic.fields import Field from pydantic import BaseModel
from pydantic.schema import field_schema, get_model_name_map from pydantic.schema import field_schema, get_model_name_map
from pydantic.utils import lenient_issubclass from pydantic.utils import lenient_issubclass
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
from starlette.routing import BaseRoute from starlette.routing import BaseRoute
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
try:
from pydantic.fields import ModelField
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic.fields import Field as ModelField # type: ignore
validation_error_definition = { validation_error_definition = {
"title": "ValidationError", "title": "ValidationError",
"type": "object", "type": "object",
@ -57,7 +64,7 @@ status_code_ranges: Dict[str, str] = {
} }
def get_openapi_params(dependant: Dependant) -> List[Field]: def get_openapi_params(dependant: Dependant) -> List[ModelField]:
flat_dependant = get_flat_dependant(dependant, skip_repeats=True) flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
return ( return (
flat_dependant.path_params flat_dependant.path_params
@ -83,37 +90,37 @@ def get_openapi_security_definitions(flat_dependant: Dependant) -> Tuple[Dict, L
def get_openapi_operation_parameters( def get_openapi_operation_parameters(
all_route_params: Sequence[Field], all_route_params: Sequence[ModelField],
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
parameters = [] parameters = []
for param in all_route_params: for param in all_route_params:
schema = param.schema field_info = get_field_info(param)
schema = cast(Param, schema) field_info = cast(Param, field_info)
parameter = { parameter = {
"name": param.alias, "name": param.alias,
"in": schema.in_.value, "in": field_info.in_.value,
"required": param.required, "required": param.required,
"schema": field_schema(param, model_name_map={})[0], "schema": field_schema(param, model_name_map={})[0],
} }
if schema.description: if field_info.description:
parameter["description"] = schema.description parameter["description"] = field_info.description
if schema.deprecated: if field_info.deprecated:
parameter["deprecated"] = schema.deprecated parameter["deprecated"] = field_info.deprecated
parameters.append(parameter) parameters.append(parameter)
return parameters return parameters
def get_openapi_operation_request_body( def get_openapi_operation_request_body(
*, body_field: Optional[Field], model_name_map: Dict[Type, str] *, body_field: Optional[ModelField], model_name_map: Dict[Type[BaseModel], str]
) -> Optional[Dict]: ) -> Optional[Dict]:
if not body_field: if not body_field:
return None return None
assert isinstance(body_field, Field) assert isinstance(body_field, ModelField)
body_schema, _, _ = field_schema( body_schema, _, _ = field_schema(
body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
) )
body_field.schema = cast(Body, body_field.schema) field_info = cast(Body, get_field_info(body_field))
request_media_type = body_field.schema.media_type request_media_type = field_info.media_type
required = body_field.required required = body_field.required
request_body_oai: Dict[str, Any] = {} request_body_oai: Dict[str, Any] = {}
if required: if required:

10
fastapi/params.py

@ -1,7 +1,11 @@
from enum import Enum from enum import Enum
from typing import Any, Callable, Sequence from typing import Any, Callable, Sequence
from pydantic import Schema try:
from pydantic.fields import FieldInfo
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic import Schema as FieldInfo # type: ignore
class ParamTypes(Enum): class ParamTypes(Enum):
@ -11,7 +15,7 @@ class ParamTypes(Enum):
cookie = "cookie" cookie = "cookie"
class Param(Schema): class Param(FieldInfo):
in_: ParamTypes in_: ParamTypes
def __init__( def __init__(
@ -199,7 +203,7 @@ class Cookie(Param):
) )
class Body(Schema): class Body(FieldInfo):
def __init__( def __init__(
self, self,
default: Any, default: Any,

206
fastapi/routing.py

@ -14,10 +14,15 @@ from fastapi.dependencies.utils import (
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from fastapi.openapi.constants import STATUS_CODES_WITH_NO_BODY from fastapi.openapi.constants import STATUS_CODES_WITH_NO_BODY
from fastapi.utils import create_cloned_field, generate_operation_id_for_path from fastapi.utils import (
from pydantic import BaseConfig, BaseModel, Schema PYDANTIC_1,
create_cloned_field,
generate_operation_id_for_path,
get_field_info,
warning_response_model_skip_defaults_deprecated,
)
from pydantic import BaseConfig, BaseModel
from pydantic.error_wrappers import ErrorWrapper, ValidationError from pydantic.error_wrappers import ErrorWrapper, ValidationError
from pydantic.fields import Field
from pydantic.utils import lenient_issubclass from pydantic.utils import lenient_issubclass
from starlette import routing from starlette import routing
from starlette.concurrency import run_in_threadpool from starlette.concurrency import run_in_threadpool
@ -34,20 +39,30 @@ from starlette.status import WS_1008_POLICY_VIOLATION
from starlette.types import ASGIApp from starlette.types import ASGIApp
from starlette.websockets import WebSocket from starlette.websockets import WebSocket
try:
from pydantic.fields import FieldInfo, ModelField
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic import Schema as FieldInfo # type: ignore
from pydantic.fields import Field as ModelField # type: ignore
def serialize_response( def serialize_response(
*, *,
field: Field = None, field: ModelField = None,
response: Response, response: Response,
include: Union[SetIntStr, DictIntStrAny] = None, include: Union[SetIntStr, DictIntStrAny] = None,
exclude: Union[SetIntStr, DictIntStrAny] = set(), exclude: Union[SetIntStr, DictIntStrAny] = set(),
by_alias: bool = True, by_alias: bool = True,
skip_defaults: bool = False, exclude_unset: bool = False,
) -> Any: ) -> Any:
if field: if field:
errors = [] errors = []
if skip_defaults and isinstance(response, BaseModel): if exclude_unset and isinstance(response, BaseModel):
response = response.dict(skip_defaults=skip_defaults) if PYDANTIC_1:
response = response.dict(exclude_unset=exclude_unset)
else:
response = response.dict(skip_defaults=exclude_unset) # pragma: nocover
value, errors_ = field.validate(response, {}, loc=("response",)) value, errors_ = field.validate(response, {}, loc=("response",))
if isinstance(errors_, ErrorWrapper): if isinstance(errors_, ErrorWrapper):
errors.append(errors_) errors.append(errors_)
@ -60,7 +75,7 @@ def serialize_response(
include=include, include=include,
exclude=exclude, exclude=exclude,
by_alias=by_alias, by_alias=by_alias,
skip_defaults=skip_defaults, exclude_unset=exclude_unset,
) )
else: else:
return jsonable_encoder(response) return jsonable_encoder(response)
@ -68,19 +83,19 @@ def serialize_response(
def get_request_handler( def get_request_handler(
dependant: Dependant, dependant: Dependant,
body_field: Field = None, body_field: ModelField = None,
status_code: int = 200, status_code: int = 200,
response_class: Type[Response] = JSONResponse, response_class: Type[Response] = JSONResponse,
response_field: Field = None, response_field: ModelField = None,
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_exclude_unset: bool = False,
dependency_overrides_provider: Any = None, dependency_overrides_provider: Any = None,
) -> Callable: ) -> Callable:
assert dependant.call is not None, "dependant.call must be a function" assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = asyncio.iscoroutinefunction(dependant.call) is_coroutine = asyncio.iscoroutinefunction(dependant.call)
is_body_form = body_field and isinstance(body_field.schema, params.Form) is_body_form = body_field and isinstance(get_field_info(body_field), params.Form)
async def app(request: Request) -> Response: async def app(request: Request) -> Response:
try: try:
@ -122,7 +137,7 @@ def get_request_handler(
include=response_model_include, include=response_model_include,
exclude=response_model_exclude, exclude=response_model_exclude,
by_alias=response_model_by_alias, by_alias=response_model_by_alias,
skip_defaults=response_model_skip_defaults, exclude_unset=response_model_exclude_unset,
) )
response = response_class( response = response_class(
content=response_data, content=response_data,
@ -199,7 +214,7 @@ class APIRoute(routing.Route):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Optional[Type[Response]] = None, response_class: Optional[Type[Response]] = None,
dependency_overrides_provider: Any = None, dependency_overrides_provider: Any = None,
@ -220,15 +235,26 @@ class APIRoute(routing.Route):
status_code not in STATUS_CODES_WITH_NO_BODY status_code not in STATUS_CODES_WITH_NO_BODY
), f"Status code {status_code} must not have a response body" ), f"Status code {status_code} must not have a response body"
response_name = "Response_" + self.unique_id response_name = "Response_" + self.unique_id
self.response_field: Optional[Field] = Field( if PYDANTIC_1:
name=response_name, self.response_field: Optional[ModelField] = ModelField(
type_=self.response_model, name=response_name,
class_validators={}, type_=self.response_model,
default=None, class_validators={},
required=False, default=None,
model_config=BaseConfig, required=False,
schema=Schema(None), model_config=BaseConfig,
) field_info=FieldInfo(None),
)
else:
self.response_field: Optional[ModelField] = ModelField( # type: ignore # pragma: nocover
name=response_name,
type_=self.response_model,
class_validators={},
default=None,
required=False,
model_config=BaseConfig,
schema=FieldInfo(None),
)
# Create a clone of the field, so that a Pydantic submodel is not returned # Create a clone of the field, so that a Pydantic submodel is not returned
# as is just because it's an instance of a subclass of a more limited class # as is just because it's an instance of a subclass of a more limited class
# e.g. UserInDB (containing hashed_password) could be a subclass of User # e.g. UserInDB (containing hashed_password) could be a subclass of User
@ -236,9 +262,9 @@ class APIRoute(routing.Route):
# would pass the validation and be returned as is. # would pass the validation and be returned as is.
# By being a new field, no inheritance will be passed as is. A new model # By being a new field, no inheritance will be passed as is. A new model
# will be always created. # will be always created.
self.secure_cloned_response_field: Optional[Field] = create_cloned_field( self.secure_cloned_response_field: Optional[
self.response_field ModelField
) ] = create_cloned_field(self.response_field)
else: else:
self.response_field = None self.response_field = None
self.secure_cloned_response_field = None self.secure_cloned_response_field = None
@ -267,18 +293,29 @@ class APIRoute(routing.Route):
model, BaseModel model, BaseModel
), "A response model must be a Pydantic model" ), "A response model must be a Pydantic model"
response_name = f"Response_{additional_status_code}_{self.unique_id}" response_name = f"Response_{additional_status_code}_{self.unique_id}"
response_field = Field( if PYDANTIC_1:
name=response_name, response_field = ModelField(
type_=model, name=response_name,
class_validators=None, type_=model,
default=None, class_validators=None,
required=False, default=None,
model_config=BaseConfig, required=False,
schema=Schema(None), model_config=BaseConfig,
) field_info=FieldInfo(None),
)
else:
response_field = ModelField( # type: ignore # pragma: nocover
name=response_name,
type_=model,
class_validators=None,
default=None,
required=False,
model_config=BaseConfig,
schema=FieldInfo(None),
)
response_fields[additional_status_code] = response_field response_fields[additional_status_code] = response_field
if response_fields: if response_fields:
self.response_fields: Dict[Union[int, str], Field] = response_fields self.response_fields: Dict[Union[int, str], ModelField] = response_fields
else: else:
self.response_fields = {} self.response_fields = {}
self.deprecated = deprecated self.deprecated = deprecated
@ -286,7 +323,7 @@ class APIRoute(routing.Route):
self.response_model_include = response_model_include self.response_model_include = response_model_include
self.response_model_exclude = response_model_exclude self.response_model_exclude = response_model_exclude
self.response_model_by_alias = response_model_by_alias self.response_model_by_alias = response_model_by_alias
self.response_model_skip_defaults = response_model_skip_defaults self.response_model_exclude_unset = response_model_exclude_unset
self.include_in_schema = include_in_schema self.include_in_schema = include_in_schema
self.response_class = response_class self.response_class = response_class
@ -313,7 +350,7 @@ class APIRoute(routing.Route):
response_model_include=self.response_model_include, response_model_include=self.response_model_include,
response_model_exclude=self.response_model_exclude, response_model_exclude=self.response_model_exclude,
response_model_by_alias=self.response_model_by_alias, response_model_by_alias=self.response_model_by_alias,
response_model_skip_defaults=self.response_model_skip_defaults, response_model_exclude_unset=self.response_model_exclude_unset,
dependency_overrides_provider=self.dependency_overrides_provider, dependency_overrides_provider=self.dependency_overrides_provider,
) )
@ -352,12 +389,15 @@ class APIRouter(routing.Router):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
route_class_override: Optional[Type[APIRoute]] = None, route_class_override: Optional[Type[APIRoute]] = None,
) -> None: ) -> None:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
route_class = route_class_override or self.route_class route_class = route_class_override or self.route_class
route = route_class( route = route_class(
path, path,
@ -376,7 +416,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class, response_class=response_class,
name=name, name=name,
@ -402,11 +444,15 @@ class APIRouter(routing.Router):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
def decorator(func: Callable) -> Callable: def decorator(func: Callable) -> Callable:
self.add_api_route( self.add_api_route(
path, path,
@ -425,7 +471,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class, response_class=response_class,
name=name, name=name,
@ -493,7 +541,7 @@ class APIRouter(routing.Router):
response_model_include=route.response_model_include, response_model_include=route.response_model_include,
response_model_exclude=route.response_model_exclude, response_model_exclude=route.response_model_exclude,
response_model_by_alias=route.response_model_by_alias, response_model_by_alias=route.response_model_by_alias,
response_model_skip_defaults=route.response_model_skip_defaults, response_model_exclude_unset=route.response_model_exclude_unset,
include_in_schema=route.include_in_schema, include_in_schema=route.include_in_schema,
response_class=route.response_class or default_response_class, response_class=route.response_class or default_response_class,
name=route.name, name=route.name,
@ -533,11 +581,14 @@ class APIRouter(routing.Router):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.api_route( return self.api_route(
path=path, path=path,
response_model=response_model, response_model=response_model,
@ -554,7 +605,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class, response_class=response_class,
name=name, name=name,
@ -577,11 +630,14 @@ class APIRouter(routing.Router):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.api_route( return self.api_route(
path=path, path=path,
response_model=response_model, response_model=response_model,
@ -598,7 +654,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class, response_class=response_class,
name=name, name=name,
@ -621,11 +679,14 @@ class APIRouter(routing.Router):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.api_route( return self.api_route(
path=path, path=path,
response_model=response_model, response_model=response_model,
@ -642,7 +703,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class, response_class=response_class,
name=name, name=name,
@ -665,11 +728,14 @@ class APIRouter(routing.Router):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.api_route( return self.api_route(
path=path, path=path,
response_model=response_model, response_model=response_model,
@ -686,7 +752,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class, response_class=response_class,
name=name, name=name,
@ -709,11 +777,14 @@ class APIRouter(routing.Router):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.api_route( return self.api_route(
path=path, path=path,
response_model=response_model, response_model=response_model,
@ -730,7 +801,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class, response_class=response_class,
name=name, name=name,
@ -753,11 +826,14 @@ class APIRouter(routing.Router):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.api_route( return self.api_route(
path=path, path=path,
response_model=response_model, response_model=response_model,
@ -774,7 +850,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class, response_class=response_class,
name=name, name=name,
@ -797,11 +875,14 @@ class APIRouter(routing.Router):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.api_route( return self.api_route(
path=path, path=path,
response_model=response_model, response_model=response_model,
@ -818,7 +899,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class, response_class=response_class,
name=name, name=name,
@ -841,11 +924,14 @@ class APIRouter(routing.Router):
response_model_include: Union[SetIntStr, DictIntStrAny] = None, response_model_include: Union[SetIntStr, DictIntStrAny] = None,
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
response_model_by_alias: bool = True, response_model_by_alias: bool = True,
response_model_skip_defaults: bool = False, response_model_skip_defaults: bool = None,
response_model_exclude_unset: bool = False,
include_in_schema: bool = True, include_in_schema: bool = True,
response_class: Type[Response] = None, response_class: Type[Response] = None,
name: str = None, name: str = None,
) -> Callable: ) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
return self.api_route( return self.api_route(
path=path, path=path,
response_model=response_model, response_model=response_model,
@ -862,7 +948,9 @@ class APIRouter(routing.Router):
response_model_include=response_model_include, response_model_include=response_model_include,
response_model_exclude=response_model_exclude, response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias, response_model_by_alias=response_model_by_alias,
response_model_skip_defaults=response_model_skip_defaults, response_model_exclude_unset=bool(
response_model_exclude_unset or response_model_skip_defaults
),
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
response_class=response_class, response_class=response_class,
name=name, name=name,

94
fastapi/utils.py

@ -1,26 +1,60 @@
import logging
import re import re
from dataclasses import is_dataclass from dataclasses import is_dataclass
from typing import Any, Dict, List, Sequence, Set, Type, cast from typing import Any, Dict, List, Sequence, Set, Type, cast
from fastapi import routing from fastapi import routing
from fastapi.openapi.constants import REF_PREFIX from fastapi.openapi.constants import REF_PREFIX
from pydantic import BaseConfig, BaseModel, Schema, create_model from pydantic import BaseConfig, BaseModel, create_model
from pydantic.fields import Field
from pydantic.schema import get_flat_models_from_fields, model_process_schema from pydantic.schema import get_flat_models_from_fields, model_process_schema
from pydantic.utils import lenient_issubclass from pydantic.utils import lenient_issubclass
from starlette.routing import BaseRoute from starlette.routing import BaseRoute
logger = logging.getLogger("fastapi")
try:
from pydantic.fields import FieldInfo, ModelField
PYDANTIC_1 = True
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic.fields import Field as ModelField # type: ignore
from pydantic import Schema as FieldInfo # type: ignore
logger.warning(
"Pydantic versions < 1.0.0 are deprecated in FastAPI and support will be \
removed soon"
)
PYDANTIC_1 = False
# TODO: remove when removing support for Pydantic < 1.0.0
def get_field_info(field: ModelField) -> FieldInfo:
if PYDANTIC_1:
return field.field_info # type: ignore
else:
return field.schema # type: ignore # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
def warning_response_model_skip_defaults_deprecated() -> None:
logger.warning( # pragma: nocover
"response_model_skip_defaults has been deprecated in favor \
of response_model_exclude_unset to keep in line with Pydantic v1, \
support for it will be removed soon."
)
def get_flat_models_from_routes(routes: Sequence[BaseRoute]) -> Set[Type[BaseModel]]: def get_flat_models_from_routes(routes: Sequence[BaseRoute]) -> Set[Type[BaseModel]]:
body_fields_from_routes: List[Field] = [] body_fields_from_routes: List[ModelField] = []
responses_from_routes: List[Field] = [] responses_from_routes: List[ModelField] = []
for route in routes: for route in routes:
if getattr(route, "include_in_schema", None) and isinstance( if getattr(route, "include_in_schema", None) and isinstance(
route, routing.APIRoute route, routing.APIRoute
): ):
if route.body_field: if route.body_field:
assert isinstance( assert isinstance(
route.body_field, Field route.body_field, ModelField
), "A request body must be a Pydantic Field" ), "A request body must be a Pydantic Field"
body_fields_from_routes.append(route.body_field) body_fields_from_routes.append(route.body_field)
if route.response_field: if route.response_field:
@ -51,7 +85,7 @@ def get_path_param_names(path: str) -> Set[str]:
return {item.strip("{}") for item in re.findall("{[^}]*}", path)} return {item.strip("{}") for item in re.findall("{[^}]*}", path)}
def create_cloned_field(field: Field) -> Field: def create_cloned_field(field: ModelField) -> ModelField:
original_type = field.type_ original_type = field.type_
if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"): if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"):
original_type = original_type.__pydantic_model__ # type: ignore original_type = original_type.__pydantic_model__ # type: ignore
@ -64,22 +98,36 @@ def create_cloned_field(field: Field) -> Field:
for f in original_type.__fields__.values(): for f in original_type.__fields__.values():
use_type.__fields__[f.name] = f use_type.__fields__[f.name] = f
use_type.__validators__ = original_type.__validators__ use_type.__validators__ = original_type.__validators__
new_field = Field( if PYDANTIC_1:
name=field.name, new_field = ModelField(
type_=use_type, name=field.name,
class_validators={}, type_=use_type,
default=None, class_validators={},
required=False, default=None,
model_config=BaseConfig, required=False,
schema=Schema(None), model_config=BaseConfig,
) field_info=FieldInfo(None),
)
else: # pragma: nocover
new_field = ModelField( # type: ignore
name=field.name,
type_=use_type,
class_validators={},
default=None,
required=False,
model_config=BaseConfig,
schema=FieldInfo(None),
)
new_field.has_alias = field.has_alias new_field.has_alias = field.has_alias
new_field.alias = field.alias new_field.alias = field.alias
new_field.class_validators = field.class_validators new_field.class_validators = field.class_validators
new_field.default = field.default new_field.default = field.default
new_field.required = field.required new_field.required = field.required
new_field.model_config = field.model_config new_field.model_config = field.model_config
new_field.schema = field.schema if PYDANTIC_1:
new_field.field_info = field.field_info
else: # pragma: nocover
new_field.schema = field.schema # type: ignore
new_field.allow_none = field.allow_none new_field.allow_none = field.allow_none
new_field.validate_always = field.validate_always new_field.validate_always = field.validate_always
if field.sub_fields: if field.sub_fields:
@ -89,11 +137,19 @@ def create_cloned_field(field: Field) -> Field:
if field.key_field: if field.key_field:
new_field.key_field = create_cloned_field(field.key_field) new_field.key_field = create_cloned_field(field.key_field)
new_field.validators = field.validators new_field.validators = field.validators
new_field.whole_pre_validators = field.whole_pre_validators if PYDANTIC_1:
new_field.whole_post_validators = field.whole_post_validators new_field.pre_validators = field.pre_validators
new_field.post_validators = field.post_validators
else: # pragma: nocover
new_field.whole_pre_validators = field.whole_pre_validators # type: ignore
new_field.whole_post_validators = field.whole_post_validators # type: ignore
new_field.parse_json = field.parse_json new_field.parse_json = field.parse_json
new_field.shape = field.shape new_field.shape = field.shape
new_field._populate_validators() try:
new_field.populate_validators()
except AttributeError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
new_field._populate_validators() # type: ignore
return new_field return new_field

2
pyproject.toml

@ -20,7 +20,7 @@ classifiers = [
] ]
requires = [ requires = [
"starlette >=0.12.9,<=0.12.9", "starlette >=0.12.9,<=0.12.9",
"pydantic >=0.32.2,<=0.32.2" "pydantic >=0.32.2,<2.0.0"
] ]
description-file = "README.md" description-file = "README.md"
requires-python = ">=3.6" requires-python = ">=3.6"

48
tests/test_application.py

@ -68,7 +68,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id"}, "schema": {"title": "Item Id"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -98,7 +98,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -128,7 +128,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "integer"}, "schema": {"title": "Item Id", "type": "integer"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -158,7 +158,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "number"}, "schema": {"title": "Item Id", "type": "number"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -188,7 +188,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "boolean"}, "schema": {"title": "Item Id", "type": "boolean"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -218,7 +218,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -248,7 +248,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -279,7 +279,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"minLength": 3, "minLength": 3,
"type": "string", "type": "string",
}, },
@ -313,7 +313,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"maxLength": 3, "maxLength": 3,
"type": "string", "type": "string",
}, },
@ -347,7 +347,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"maxLength": 3, "maxLength": 3,
"minLength": 2, "minLength": 2,
"type": "string", "type": "string",
@ -382,7 +382,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"exclusiveMinimum": 3.0, "exclusiveMinimum": 3.0,
"type": "number", "type": "number",
}, },
@ -416,7 +416,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"exclusiveMinimum": 0.0, "exclusiveMinimum": 0.0,
"type": "number", "type": "number",
}, },
@ -450,7 +450,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"minimum": 3.0, "minimum": 3.0,
"type": "number", "type": "number",
}, },
@ -484,7 +484,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"exclusiveMaximum": 3.0, "exclusiveMaximum": 3.0,
"type": "number", "type": "number",
}, },
@ -518,7 +518,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"exclusiveMaximum": 0.0, "exclusiveMaximum": 0.0,
"type": "number", "type": "number",
}, },
@ -552,7 +552,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"maximum": 3.0, "maximum": 3.0,
"type": "number", "type": "number",
}, },
@ -586,7 +586,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"exclusiveMaximum": 3.0, "exclusiveMaximum": 3.0,
"exclusiveMinimum": 1.0, "exclusiveMinimum": 1.0,
"type": "number", "type": "number",
@ -621,7 +621,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"maximum": 3.0, "maximum": 3.0,
"minimum": 1.0, "minimum": 1.0,
"type": "number", "type": "number",
@ -656,7 +656,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"exclusiveMaximum": 3.0, "exclusiveMaximum": 3.0,
"type": "integer", "type": "integer",
}, },
@ -690,7 +690,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"exclusiveMinimum": 3.0, "exclusiveMinimum": 3.0,
"type": "integer", "type": "integer",
}, },
@ -724,7 +724,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"maximum": 3.0, "maximum": 3.0,
"type": "integer", "type": "integer",
}, },
@ -758,7 +758,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"minimum": 3.0, "minimum": 3.0,
"type": "integer", "type": "integer",
}, },
@ -792,7 +792,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"exclusiveMaximum": 3.0, "exclusiveMaximum": 3.0,
"exclusiveMinimum": 1.0, "exclusiveMinimum": 1.0,
"type": "integer", "type": "integer",
@ -827,7 +827,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"maximum": 3.0, "maximum": 3.0,
"minimum": 1.0, "minimum": 1.0,
"type": "integer", "type": "integer",

14
tests/test_extra_routes.py

@ -77,7 +77,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -105,7 +105,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -141,7 +141,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -169,7 +169,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -197,7 +197,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -233,7 +233,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -263,7 +263,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }

4
tests/test_infer_param_optionality.py

@ -110,7 +110,7 @@ def test_schema_1():
d = { d = {
"required": True, "required": True,
"schema": {"title": "User_Id", "type": "string"}, "schema": {"title": "User Id", "type": "string"},
"name": "user_id", "name": "user_id",
"in": "path", "in": "path",
} }
@ -127,7 +127,7 @@ def test_schema_2():
d = { d = {
"required": False, "required": False,
"schema": {"title": "User_Id", "type": "string"}, "schema": {"title": "User Id", "type": "string"},
"name": "user_id", "name": "user_id",
"in": "query", "in": "query",
} }

10
tests/test_jsonable_encoder.py

@ -3,7 +3,13 @@ from enum import Enum
import pytest import pytest
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, Schema, ValidationError from pydantic import BaseModel, ValidationError
try:
from pydantic import Field
except ImportError: # pragma: nocover
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic import Schema as Field
class Person: class Person:
@ -60,7 +66,7 @@ class ModelWithConfig(BaseModel):
class ModelWithAlias(BaseModel): class ModelWithAlias(BaseModel):
foo: str = Schema(..., alias="Foo") foo: str = Field(..., alias="Foo")
def test_encode_class(): def test_encode_class():

16
tests/test_path.py

@ -18,6 +18,16 @@ def test_nonexistent():
assert response.json() == {"detail": "Not Found"} assert response.json() == {"detail": "Not Found"}
response_not_valid_bool = {
"detail": [
{
"loc": ["path", "item_id"],
"msg": "value could not be parsed to a boolean",
"type": "type_error.bool",
}
]
}
response_not_valid_int = { response_not_valid_int = {
"detail": [ "detail": [
{ {
@ -173,10 +183,10 @@ response_less_than_equal_3 = {
("/path/float/True", 422, response_not_valid_float), ("/path/float/True", 422, response_not_valid_float),
("/path/float/42", 200, 42), ("/path/float/42", 200, 42),
("/path/float/42.5", 200, 42.5), ("/path/float/42.5", 200, 42.5),
("/path/bool/foobar", 200, False), ("/path/bool/foobar", 422, response_not_valid_bool),
("/path/bool/True", 200, True), ("/path/bool/True", 200, True),
("/path/bool/42", 200, False), ("/path/bool/42", 422, response_not_valid_bool),
("/path/bool/42.5", 200, False), ("/path/bool/42.5", 422, response_not_valid_bool),
("/path/bool/1", 200, True), ("/path/bool/1", 200, True),
("/path/bool/0", 200, False), ("/path/bool/0", 200, False),
("/path/bool/true", 200, True), ("/path/bool/true", 200, True),

2
tests/test_put_no_body.py

@ -39,7 +39,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }

6
tests/test_security_oauth2.py

@ -99,15 +99,15 @@ openapi_schema = {
"type": "object", "type": "object",
"properties": { "properties": {
"grant_type": { "grant_type": {
"title": "Grant_Type", "title": "Grant Type",
"pattern": "password", "pattern": "password",
"type": "string", "type": "string",
}, },
"username": {"title": "Username", "type": "string"}, "username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"}, "password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""}, "scope": {"title": "Scope", "type": "string", "default": ""},
"client_id": {"title": "Client_Id", "type": "string"}, "client_id": {"title": "Client Id", "type": "string"},
"client_secret": {"title": "Client_Secret", "type": "string"}, "client_secret": {"title": "Client Secret", "type": "string"},
}, },
}, },
"ValidationError": { "ValidationError": {

6
tests/test_security_oauth2_optional.py

@ -103,15 +103,15 @@ openapi_schema = {
"type": "object", "type": "object",
"properties": { "properties": {
"grant_type": { "grant_type": {
"title": "Grant_Type", "title": "Grant Type",
"pattern": "password", "pattern": "password",
"type": "string", "type": "string",
}, },
"username": {"title": "Username", "type": "string"}, "username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"}, "password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""}, "scope": {"title": "Scope", "type": "string", "default": ""},
"client_id": {"title": "Client_Id", "type": "string"}, "client_id": {"title": "Client Id", "type": "string"},
"client_secret": {"title": "Client_Secret", "type": "string"}, "client_secret": {"title": "Client Secret", "type": "string"},
}, },
}, },
"ValidationError": { "ValidationError": {

2
tests/test_skip_defaults.py

@ -20,7 +20,7 @@ class ModelSubclass(Model):
y: int y: int
@app.get("/", response_model=Model, response_model_skip_defaults=True) @app.get("/", response_model=Model, response_model_exclude_unset=True)
def get() -> ModelSubclass: def get() -> ModelSubclass:
return ModelSubclass(sub={}, y=1) return ModelSubclass(sub={}, y=1)

4
tests/test_starlette_exception.py

@ -54,7 +54,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -84,7 +84,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }

2
tests/test_tutorial/test_additional_responses/test_tutorial001.py

@ -43,7 +43,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }

2
tests/test_tutorial/test_additional_responses/test_tutorial002.py

@ -39,7 +39,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
}, },

2
tests/test_tutorial/test_additional_responses/test_tutorial003.py

@ -44,7 +44,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }

2
tests/test_tutorial/test_additional_responses/test_tutorial004.py

@ -42,7 +42,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
}, },

2
tests/test_tutorial/test_async_sql_databases/test_tutorial001.py

@ -14,7 +14,7 @@ openapi_schema = {
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"title": "Response_Read_Notes_Notes__Get", "title": "Response Read Notes Notes Get",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/Note"}, "items": {"$ref": "#/components/schemas/Note"},
} }

4
tests/test_tutorial/test_bigger_applications/test_main.py

@ -123,7 +123,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
}, },
@ -160,7 +160,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
}, },

4
tests/test_tutorial/test_body_multiple_params/test_tutorial003.py

@ -32,7 +32,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "integer"}, "schema": {"title": "Item Id", "type": "integer"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -69,7 +69,7 @@ openapi_schema = {
"type": "object", "type": "object",
"properties": { "properties": {
"username": {"title": "Username", "type": "string"}, "username": {"title": "Username", "type": "string"},
"full_name": {"title": "Full_Name", "type": "string"}, "full_name": {"title": "Full Name", "type": "string"},
}, },
}, },
"Body_update_item_items__item_id__put": { "Body_update_item_items__item_id__put": {

11
tests/test_tutorial/test_body_schema/test_tutorial001.py

@ -3,6 +3,15 @@ from starlette.testclient import TestClient
from body_schema.tutorial001 import app from body_schema.tutorial001 import app
# TODO: remove when removing support for Pydantic < 1.0.0
try:
from pydantic import Field # noqa
except ImportError: # pragma: nocover
import pydantic
pydantic.Field = pydantic.Schema
client = TestClient(app) client = TestClient(app)
@ -33,7 +42,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "integer"}, "schema": {"title": "Item Id", "type": "integer"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }

4
tests/test_tutorial/test_body_updates/test_tutorial001.py

@ -35,7 +35,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -67,7 +67,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }

2
tests/test_tutorial/test_cookie_params/test_tutorial001.py

@ -32,7 +32,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Ads_Id", "type": "string"}, "schema": {"title": "Ads Id", "type": "string"},
"name": "ads_id", "name": "ads_id",
"in": "cookie", "in": "cookie",
} }

2
tests/test_tutorial/test_events/test_tutorial001.py

@ -29,7 +29,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }

10
tests/test_tutorial/test_extra_data_types/test_tutorial001.py

@ -33,7 +33,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Item_Id", "title": "Item Id",
"type": "string", "type": "string",
"format": "uuid", "format": "uuid",
}, },
@ -60,22 +60,22 @@ openapi_schema = {
"type": "object", "type": "object",
"properties": { "properties": {
"start_datetime": { "start_datetime": {
"title": "Start_Datetime", "title": "Start Datetime",
"type": "string", "type": "string",
"format": "date-time", "format": "date-time",
}, },
"end_datetime": { "end_datetime": {
"title": "End_Datetime", "title": "End Datetime",
"type": "string", "type": "string",
"format": "date-time", "format": "date-time",
}, },
"repeat_at": { "repeat_at": {
"title": "Repeat_At", "title": "Repeat At",
"type": "string", "type": "string",
"format": "time", "format": "time",
}, },
"process_after": { "process_after": {
"title": "Process_After", "title": "Process After",
"type": "number", "type": "number",
"format": "time-delta", "format": "time-delta",
}, },

4
tests/test_tutorial/test_extra_models/test_tutorial003.py

@ -16,7 +16,7 @@ openapi_schema = {
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"title": "Response_Read_Item_Items__Item_Id__Get", "title": "Response Read Item Items Item Id Get",
"anyOf": [ "anyOf": [
{"$ref": "#/components/schemas/PlaneItem"}, {"$ref": "#/components/schemas/PlaneItem"},
{"$ref": "#/components/schemas/CarItem"}, {"$ref": "#/components/schemas/CarItem"},
@ -41,7 +41,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }

2
tests/test_tutorial/test_extra_models/test_tutorial004.py

@ -16,7 +16,7 @@ openapi_schema = {
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"title": "Response_Read_Items_Items__Get", "title": "Response Read Items Items Get",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/Item"}, "items": {"$ref": "#/components/schemas/Item"},
} }

2
tests/test_tutorial/test_extra_models/test_tutorial005.py

@ -16,7 +16,7 @@ openapi_schema = {
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"title": "Response_Read_Keyword_Weights_Keyword-Weights__Get", "title": "Response Read Keyword Weights Keyword-Weights Get",
"type": "object", "type": "object",
"additionalProperties": {"type": "number"}, "additionalProperties": {"type": "number"},
} }

2
tests/test_tutorial/test_handling_errors/test_tutorial001.py

@ -31,7 +31,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }

2
tests/test_tutorial/test_handling_errors/test_tutorial002.py

@ -31,7 +31,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }

2
tests/test_tutorial/test_handling_errors/test_tutorial004.py

@ -31,7 +31,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "integer"}, "schema": {"title": "Item Id", "type": "integer"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }

2
tests/test_tutorial/test_handling_errors/test_tutorial005.py

@ -31,7 +31,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "integer"}, "schema": {"title": "Item Id", "type": "integer"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }

2
tests/test_tutorial/test_path_params/test_tutorial004.py

@ -31,7 +31,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "File_Path", "type": "string"}, "schema": {"title": "File Path", "type": "string"},
"name": "file_path", "name": "file_path",
"in": "path", "in": "path",
} }

2
tests/test_tutorial/test_path_params/test_tutorial005.py

@ -33,7 +33,7 @@ openapi_schema = {
{ {
"required": True, "required": True,
"schema": { "schema": {
"title": "Model_Name", "title": "Model Name",
"enum": ["alexnet", "resnet", "lenet"], "enum": ["alexnet", "resnet", "lenet"],
"type": "string", "type": "string",
}, },

2
tests/test_tutorial/test_query_params/test_tutorial005.py

@ -32,7 +32,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
}, },

2
tests/test_tutorial/test_query_params/test_tutorial006.py

@ -32,7 +32,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
}, },

2
tests/test_tutorial/test_query_params/test_tutorial007.py

@ -31,7 +31,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
}, },

4
tests/test_tutorial/test_response_model/test_tutorial003.py

@ -52,7 +52,7 @@ openapi_schema = {
"properties": { "properties": {
"username": {"title": "Username", "type": "string"}, "username": {"title": "Username", "type": "string"},
"email": {"title": "Email", "type": "string", "format": "email"}, "email": {"title": "Email", "type": "string", "format": "email"},
"full_name": {"title": "Full_Name", "type": "string"}, "full_name": {"title": "Full Name", "type": "string"},
}, },
}, },
"UserIn": { "UserIn": {
@ -63,7 +63,7 @@ openapi_schema = {
"username": {"title": "Username", "type": "string"}, "username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"}, "password": {"title": "Password", "type": "string"},
"email": {"title": "Email", "type": "string", "format": "email"}, "email": {"title": "Email", "type": "string", "format": "email"},
"full_name": {"title": "Full_Name", "type": "string"}, "full_name": {"title": "Full Name", "type": "string"},
}, },
}, },
"ValidationError": { "ValidationError": {

2
tests/test_tutorial/test_response_model/test_tutorial004.py

@ -36,7 +36,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }

4
tests/test_tutorial/test_response_model/test_tutorial005.py

@ -35,7 +35,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -69,7 +69,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }

4
tests/test_tutorial/test_response_model/test_tutorial006.py

@ -35,7 +35,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }
@ -69,7 +69,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "Item_Id", "type": "string"}, "schema": {"title": "Item Id", "type": "string"},
"name": "item_id", "name": "item_id",
"in": "path", "in": "path",
} }

6
tests/test_tutorial/test_security/test_tutorial003.py

@ -62,15 +62,15 @@ openapi_schema = {
"type": "object", "type": "object",
"properties": { "properties": {
"grant_type": { "grant_type": {
"title": "Grant_Type", "title": "Grant Type",
"pattern": "password", "pattern": "password",
"type": "string", "type": "string",
}, },
"username": {"title": "Username", "type": "string"}, "username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"}, "password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""}, "scope": {"title": "Scope", "type": "string", "default": ""},
"client_id": {"title": "Client_Id", "type": "string"}, "client_id": {"title": "Client Id", "type": "string"},
"client_secret": {"title": "Client_Secret", "type": "string"}, "client_secret": {"title": "Client Secret", "type": "string"},
}, },
}, },
"ValidationError": { "ValidationError": {

12
tests/test_tutorial/test_security/test_tutorial005.py

@ -103,7 +103,7 @@ openapi_schema = {
"properties": { "properties": {
"username": {"title": "Username", "type": "string"}, "username": {"title": "Username", "type": "string"},
"email": {"title": "Email", "type": "string"}, "email": {"title": "Email", "type": "string"},
"full_name": {"title": "Full_Name", "type": "string"}, "full_name": {"title": "Full Name", "type": "string"},
"disabled": {"title": "Disabled", "type": "boolean"}, "disabled": {"title": "Disabled", "type": "boolean"},
}, },
}, },
@ -112,8 +112,8 @@ openapi_schema = {
"required": ["access_token", "token_type"], "required": ["access_token", "token_type"],
"type": "object", "type": "object",
"properties": { "properties": {
"access_token": {"title": "Access_Token", "type": "string"}, "access_token": {"title": "Access Token", "type": "string"},
"token_type": {"title": "Token_Type", "type": "string"}, "token_type": {"title": "Token Type", "type": "string"},
}, },
}, },
"Body_login_for_access_token_token_post": { "Body_login_for_access_token_token_post": {
@ -122,15 +122,15 @@ openapi_schema = {
"type": "object", "type": "object",
"properties": { "properties": {
"grant_type": { "grant_type": {
"title": "Grant_Type", "title": "Grant Type",
"pattern": "password", "pattern": "password",
"type": "string", "type": "string",
}, },
"username": {"title": "Username", "type": "string"}, "username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"}, "password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""}, "scope": {"title": "Scope", "type": "string", "default": ""},
"client_id": {"title": "Client_Id", "type": "string"}, "client_id": {"title": "Client Id", "type": "string"},
"client_secret": {"title": "Client_Secret", "type": "string"}, "client_secret": {"title": "Client Secret", "type": "string"},
}, },
}, },
"ValidationError": { "ValidationError": {

12
tests/test_tutorial/test_sql_databases/test_sql_databases.py

@ -15,7 +15,7 @@ openapi_schema = {
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"title": "Response_Read_Users_Users__Get", "title": "Response Read Users Users Get",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/User"}, "items": {"$ref": "#/components/schemas/User"},
} }
@ -110,7 +110,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "User_Id", "type": "integer"}, "schema": {"title": "User Id", "type": "integer"},
"name": "user_id", "name": "user_id",
"in": "path", "in": "path",
} }
@ -144,7 +144,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "User_Id", "type": "integer"}, "schema": {"title": "User Id", "type": "integer"},
"name": "user_id", "name": "user_id",
"in": "path", "in": "path",
} }
@ -167,7 +167,7 @@ openapi_schema = {
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"title": "Response_Read_Items_Items__Get", "title": "Response Read Items Items Get",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/Item"}, "items": {"$ref": "#/components/schemas/Item"},
} }
@ -223,7 +223,7 @@ openapi_schema = {
"title": {"title": "Title", "type": "string"}, "title": {"title": "Title", "type": "string"},
"description": {"title": "Description", "type": "string"}, "description": {"title": "Description", "type": "string"},
"id": {"title": "Id", "type": "integer"}, "id": {"title": "Id", "type": "integer"},
"owner_id": {"title": "Owner_Id", "type": "integer"}, "owner_id": {"title": "Owner Id", "type": "integer"},
}, },
}, },
"User": { "User": {
@ -233,7 +233,7 @@ openapi_schema = {
"properties": { "properties": {
"email": {"title": "Email", "type": "string"}, "email": {"title": "Email", "type": "string"},
"id": {"title": "Id", "type": "integer"}, "id": {"title": "Id", "type": "integer"},
"is_active": {"title": "Is_Active", "type": "boolean"}, "is_active": {"title": "Is Active", "type": "boolean"},
"items": { "items": {
"title": "Items", "title": "Items",
"type": "array", "type": "array",

12
tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py

@ -15,7 +15,7 @@ openapi_schema = {
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"title": "Response_Read_Users_Users__Get", "title": "Response Read Users Users Get",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/User"}, "items": {"$ref": "#/components/schemas/User"},
} }
@ -110,7 +110,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "User_Id", "type": "integer"}, "schema": {"title": "User Id", "type": "integer"},
"name": "user_id", "name": "user_id",
"in": "path", "in": "path",
} }
@ -144,7 +144,7 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": True, "required": True,
"schema": {"title": "User_Id", "type": "integer"}, "schema": {"title": "User Id", "type": "integer"},
"name": "user_id", "name": "user_id",
"in": "path", "in": "path",
} }
@ -167,7 +167,7 @@ openapi_schema = {
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"title": "Response_Read_Items_Items__Get", "title": "Response Read Items Items Get",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/Item"}, "items": {"$ref": "#/components/schemas/Item"},
} }
@ -223,7 +223,7 @@ openapi_schema = {
"title": {"title": "Title", "type": "string"}, "title": {"title": "Title", "type": "string"},
"description": {"title": "Description", "type": "string"}, "description": {"title": "Description", "type": "string"},
"id": {"title": "Id", "type": "integer"}, "id": {"title": "Id", "type": "integer"},
"owner_id": {"title": "Owner_Id", "type": "integer"}, "owner_id": {"title": "Owner Id", "type": "integer"},
}, },
}, },
"User": { "User": {
@ -233,7 +233,7 @@ openapi_schema = {
"properties": { "properties": {
"email": {"title": "Email", "type": "string"}, "email": {"title": "Email", "type": "string"},
"id": {"title": "Id", "type": "integer"}, "id": {"title": "Id", "type": "integer"},
"is_active": {"title": "Is_Active", "type": "boolean"}, "is_active": {"title": "Is Active", "type": "boolean"},
"items": { "items": {
"title": "Items", "title": "Items",
"type": "array", "type": "array",

Loading…
Cancel
Save