id: cache
with:
path: ${{ env.pythonLocation }}
- key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
+ key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pip install -r requirements-tests.txt
+ - name: Install Pydantic v2
+ run: pip install "pydantic>=2.0.2,<3.0.0"
- name: Lint
run: bash scripts/lint.sh
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+ pydantic-version: ["pydantic-v1", "pydantic-v2"]
fail-fast: false
steps:
- uses: actions/checkout@v3
id: cache
with:
path: ${{ env.pythonLocation }}
- key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
+ key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pip install -r requirements-tests.txt
+ - name: Install Pydantic v1
+ if: matrix.pydantic-version == 'pydantic-v1'
+ run: pip install "pydantic>=1.10.0,<2.0.0"
+ - name: Install Pydantic v2
+ if: matrix.pydantic-version == 'pydantic-v2'
+ run: pip install "pydantic>=2.0.2,<3.0.0"
- run: mkdir coverage
- name: Test
run: bash scripts/test.sh
Used by Pydantic:
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
+* <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - for settings management.
+* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - for extra types to be used with Pydantic.
Used by Starlette:
# Async SQL (Relational) Databases
+!!! info
+ These docs are about to be updated. 🎉
+
+ The current version assumes Pydantic v1.
+
+ The new docs will include Pydantic v2 and will use <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a> once it is updated to use Pydantic v2 as well.
+
You can also use <a href="https://github.com/encode/databases" class="external-link" target="_blank">`encode/databases`</a> with **FastAPI** to connect to databases using `async` and `await`.
It is compatible with:
# NoSQL (Distributed / Big Data) Databases
+!!! info
+ These docs are about to be updated. 🎉
+
+ The current version assumes Pydantic v1.
+
+ The new docs will hopefully use Pydantic v2 and will use <a href="https://art049.github.io/odmantic/" class="external-link" target="_blank">ODMantic</a> with MongoDB.
+
**FastAPI** can also be integrated with any <abbr title="Distributed database (Big Data), also 'Not Only SQL'">NoSQL</abbr>.
Here we'll see an example using **<a href="https://www.couchbase.com/" class="external-link" target="_blank">Couchbase</a>**, a <abbr title="Document here refers to a JSON object (a dict), with keys and values, and those values can also be other JSON objects, arrays (lists), numbers, strings, booleans, etc.">document</abbr> based NoSQL database.
For example, in this application we don't use FastAPI's integrated functionality to extract the JSON Schema from Pydantic models nor the automatic validation for JSON. In fact, we are declaring the request content type as YAML, not JSON:
-```Python hl_lines="17-22 24"
-{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!}
-```
+=== "Pydantic v2"
+
+ ```Python hl_lines="17-22 24"
+ {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007.py!}
+ ```
+
+=== "Pydantic v1"
+
+ ```Python hl_lines="17-22 24"
+ {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py!}
+ ```
+
+!!! info
+ In Pydantic version 1 the method to get the JSON Schema for a model was called `Item.schema()`, in Pydantic version 2, the method is called `Item.model_schema_json()`.
Nevertheless, although we are not using the default integrated functionality, we are still using a Pydantic model to manually generate the JSON Schema for the data that we want to receive in YAML.
And then in our code, we parse that YAML content directly, and then we are again using the same Pydantic model to validate the YAML content:
-```Python hl_lines="26-33"
-{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!}
-```
+=== "Pydantic v2"
+
+ ```Python hl_lines="26-33"
+ {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007.py!}
+ ```
+
+=== "Pydantic v1"
+
+ ```Python hl_lines="26-33"
+ {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py!}
+ ```
+
+!!! info
+ In Pydantic version 1 the method to parse and validate an object was `Item.parse_obj()`, in Pydantic version 2, the method is called `Item.model_validate()`.
!!! tip
Here we re-use the same Pydantic model.
## Pydantic `Settings`
-Fortunately, Pydantic provides a great utility to handle these settings coming from environment variables with <a href="https://pydantic-docs.helpmanual.io/usage/settings/" class="external-link" target="_blank">Pydantic: Settings management</a>.
+Fortunately, Pydantic provides a great utility to handle these settings coming from environment variables with <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" class="external-link" target="_blank">Pydantic: Settings management</a>.
+
+### Install `pydantic-settings`
+
+First, install the `pydantic-settings` package:
+
+<div class="termy">
+
+```console
+$ pip install pydantic-settings
+---> 100%
+```
+
+</div>
+
+It also comes included when you install the `all` extras with:
+
+<div class="termy">
+
+```console
+$ pip install "fastapi[all]"
+---> 100%
+```
+
+</div>
+
+!!! info
+ In Pydantic v1 it came included with the main package. Now it is distributed as this independent package so that you can choose to install it or not if you don't need that functionality.
### Create the `Settings` object
You can use all the same validation features and tools you use for Pydantic models, like different data types and additional validations with `Field()`.
-```Python hl_lines="2 5-8 11"
-{!../../../docs_src/settings/tutorial001.py!}
-```
+=== "Pydantic v2"
+
+ ```Python hl_lines="2 5-8 11"
+ {!> ../../../docs_src/settings/tutorial001.py!}
+ ```
+
+=== "Pydantic v1"
+
+ !!! info
+ In Pydantic v1 you would import `BaseSettings` directly from `pydantic` instead of from `pydantic_settings`.
+
+ ```Python hl_lines="2 5-8 11"
+ {!> ../../../docs_src/settings/tutorial001_pv1.py!}
+ ```
!!! tip
If you want something quick to copy and paste, don't use this example, use the last one below.
And then update your `config.py` with:
-```Python hl_lines="9-10"
-{!../../../docs_src/settings/app03/config.py!}
-```
+=== "Pydantic v2"
+
+ ```Python hl_lines="9"
+ {!> ../../../docs_src/settings/app03_an/config.py!}
+ ```
-Here we create a class `Config` inside of your Pydantic `Settings` class, and set the `env_file` to the filename with the dotenv file we want to use.
+ !!! tip
+ The `model_config` attribute is used just for Pydantic configuration. You can read more at <a href="https://docs.pydantic.dev/latest/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>.
-!!! tip
- The `Config` class is used just for Pydantic configuration. You can read more at <a href="https://pydantic-docs.helpmanual.io/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>
+=== "Pydantic v1"
+
+ ```Python hl_lines="9-10"
+ {!> ../../../docs_src/settings/app03_an/config_pv1.py!}
+ ```
+
+ !!! tip
+ The `Config` class is used just for Pydantic configuration. You can read more at <a href="https://docs.pydantic.dev/1.10/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>.
+
+!!! info
+ In Pydantic version 1 the configuration was done in an internal class `Config`, in Pydantic version 2 it's done in an attribute `model_config`. This attribute takes a `dict`, and to get autocompletion and inline errors you can import and use `SettingsConfigDict` to define that `dict`.
+
+Here we define the config `env_file` inside of your Pydantic `Settings` class, and set the value to the filename with the dotenv file we want to use.
### Creating the `Settings` only once with `lru_cache`
Feel free to skip this.
+ Peewee is not recommended with FastAPI as it doesn't play well with anything async Python. There are several better alternatives.
+
+!!! info
+ These docs assume Pydantic v1.
+
+ Because Pewee doesn't play well with anything async and there are better alternatives, I won't update these docs for Pydantic v2, they are kept for now only for historical purposes.
+
If you are starting a project from scratch, you are probably better off with SQLAlchemy ORM ([SQL (Relational) Databases](../tutorial/sql-databases.md){.internal-link target=_blank}), or any other async ORM.
If you already have a code base that uses <a href="https://docs.peewee-orm.com/en/latest/" class="external-link" target="_blank">Peewee ORM</a>, you can check here how to use it with **FastAPI**.
# Testing a Database
+!!! info
+ These docs are about to be updated. 🎉
+
+ The current version assumes Pydantic v1, and SQLAlchemy versions less than 2.0.
+
+ The new docs will include Pydantic v2 and will use <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a> (which is also based on SQLAlchemy) once it is updated to use Pydantic v2 as well.
+
You can use the same dependency overrides from [Testing Dependencies with Overrides](testing-dependencies.md){.internal-link target=_blank} to alter a database for testing.
You could want to set up a different database for testing, rollback the data after the tests, pre-fill it with some testing data, etc.
Used by Pydantic:
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
+* <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - for settings management.
+* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - for extra types to be used with Pydantic.
Used by Starlette:
## Latest Changes
+✨ Support for **Pydantic v2** ✨
+
+Pydantic version 2 has the **core** re-written in **Rust** and includes a lot of improvements and features, for example:
+
+* Improved **correctness** in corner cases.
+* **Safer** types.
+* Better **performance** and **less energy** consumption.
+* Better **extensibility**.
+* etc.
+
+...all this while keeping the **same Python API**. In most of the cases, for simple models, you can simply upgrade the Pydantic version and get all the benefits. 🚀
+
+In some cases, for pure data validation and processing, you can get performance improvements of **20x** or more. This means 2,000% or more. 🤯
+
+When you use **FastAPI**, there's a lot more going on, processing the request and response, handling dependencies, executing **your own code**, and particularly, **waiting for the network**. But you will probably still get some nice performance improvements just from the upgrade.
+
+The focus of this release is **compatibility** with Pydantic v1 and v2, to make sure your current apps keep working. Later there will be more focus on refactors, correctness, code improvements, and then **performance** improvements. Some third-party early beta testers that ran benchmarks on the beta releases of FastAPI reported improvements of **2x - 3x**. Which is not bad for just doing `pip install --upgrade fastapi pydantic`. This was not an official benchmark and I didn't check it myself, but it's a good sign.
+
+### Migration
+
+Check out the [Pydantic migration guide](https://docs.pydantic.dev/2.0/migration/).
+
+For the things that need changes in your Pydantic models, the Pydantic team built [`bump-pydantic`](https://github.com/pydantic/bump-pydantic).
+
+A command line tool that will **process your code** and update most of the things **automatically** for you. Make sure you have your code in git first, and review each of the changes to make sure everything is correct before committing the changes.
+
+### Pydantic v1
+
+**This version of FastAPI still supports Pydantic v1**. And although Pydantic v1 will be deprecated at some point, ti will still be supported for a while.
+
+This means that you can install the new Pydantic v2, and if something fails, you can install Pydantic v1 while you fix any problems you might have, but having the latest FastAPI.
+
+There are **tests for both Pydantic v1 and v2**, and test **coverage** is kept at **100%**.
+
+### Changes
+
+* There are **new parameter** fields supported by Pydantic `Field()` for:
+
+ * `Path()`
+ * `Query()`
+ * `Header()`
+ * `Cookie()`
+ * `Body()`
+ * `Form()`
+ * `File()`
+
+* The new parameter fields are:
+
+ * `default_factory`
+ * `alias_priority`
+ * `validation_alias`
+ * `serialization_alias`
+ * `discriminator`
+ * `strict`
+ * `multiple_of`
+ * `allow_inf_nan`
+ * `max_digits`
+ * `decimal_places`
+ * `json_schema_extra`
+
+...you can read about them in the Pydantic docs.
+
+* The parameter `regex` has been deprecated and replaced by `pattern`.
+ * You can read more about it in the docs for [Query Parameters and String Validations: Add regular expressions](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#add-regular-expressions).
+* New Pydantic models use an improved and simplified attribute `model_config` that takes a simple dict instead of an internal class `Config` for their configuration.
+ * You can read more about it in the docs for [Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/).
+* The attribute `schema_extra` for the internal class `Config` has been replaced by the key `json_schema_extra` in the new `model_config` dict.
+ * You can read more about it in the docs for [Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/).
+* When you install `"fastapi[all]"` it now also includes:
+ * <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - for settings management.
+ * <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - for extra types to be used with Pydantic.
+* Now Pydantic Settings is an additional optional package (included in `"fastapi[all]"`). To use settings you should now import `from pydantic_settings import BaseSettings` instead of importing from `pydantic` directly.
+ * You can read more about it in the docs for [Settings and Environment Variables](https://fastapi.tiangolo.com/advanced/settings/).
## 0.99.1
## Add regular expressions
-You can define a <abbr title="A regular expression, regex or regexp is a sequence of characters that define a search pattern for strings.">regular expression</abbr> that the parameter should match:
+You can define a <abbr title="A regular expression, regex or regexp is a sequence of characters that define a search pattern for strings.">regular expression</abbr> `pattern` that the parameter should match:
=== "Python 3.10+"
{!> ../../../docs_src/query_params_str_validations/tutorial004.py!}
```
-This specific regular expression checks that the received parameter value:
+This specific regular expression pattern checks that the received parameter value:
* `^`: starts with the following characters, doesn't have characters before.
* `fixedquery`: has the exact value `fixedquery`.
But whenever you need them and go and learn them, know that you can already use them directly in **FastAPI**.
+### Pydantic v1 `regex` instead of `pattern`
+
+Before Pydantic version 2 and before FastAPI 0.100.0, the parameter was called `regex` instead of `pattern`, but it's now deprecated.
+
+You could still see some code using it:
+
+=== "Python 3.10+ Pydantic v1"
+
+ ```Python hl_lines="11"
+ {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py310_regex.py!}
+ ```
+
+But know that this is deprecated and it should be updated to use the new parameter `pattern`. 🤓
+
## Default values
You can, of course, use default values other than `None`.
Here are several ways to do it.
-## Pydantic `schema_extra`
+## Extra JSON Schema data in Pydantic models
-You can declare `examples` for a Pydantic model using `Config` and `schema_extra`, as described in <a href="https://pydantic-docs.helpmanual.io/usage/schema/#schema-customization" class="external-link" target="_blank">Pydantic's docs: Schema customization</a>:
+You can declare `examples` for a Pydantic model that will be added to the generated JSON Schema.
-=== "Python 3.10+"
+=== "Python 3.10+ Pydantic v2"
- ```Python hl_lines="13-23"
+ ```Python hl_lines="13-24"
{!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!}
```
-=== "Python 3.6+"
+=== "Python 3.10+ Pydantic v1"
- ```Python hl_lines="15-25"
+ ```Python hl_lines="13-23"
+ {!> ../../../docs_src/schema_extra_example/tutorial001_py310_pv1.py!}
+ ```
+
+=== "Python 3.6+ Pydantic v2"
+
+ ```Python hl_lines="15-26"
{!> ../../../docs_src/schema_extra_example/tutorial001.py!}
```
+=== "Python 3.6+ Pydantic v1"
+
+ ```Python hl_lines="15-25"
+ {!> ../../../docs_src/schema_extra_example/tutorial001_pv1.py!}
+ ```
+
That extra info will be added as-is to the output **JSON Schema** for that model, and it will be used in the API docs.
+=== "Pydantic v2"
+
+ In Pydantic version 2, you would use the attribute `model_config`, that takes a `dict` as described in <a href="https://docs.pydantic.dev/latest/usage/model_config/" class="external-link" target="_blank">Pydantic's docs: Model Config</a>.
+
+ You can set `"json_schema_extra"` with a `dict` containing any additonal data you would like to show up in the generated JSON Schema, including `examples`.
+
+=== "Pydantic v1"
+
+ In Pydantic version 1, you would use an internal class `Config` and `schema_extra`, as described in <a href="https://docs.pydantic.dev/1.10/usage/schema/#schema-customization" class="external-link" target="_blank">Pydantic's docs: Schema customization</a>.
+
+ You can set `schema_extra` with a `dict` containing any additonal data you would like to show up in the generated JSON Schema, including `examples`.
+
!!! tip
You could use the same technique to extend the JSON Schema and add your own custom extra info.
# SQL (Relational) Databases
+!!! info
+ These docs are about to be updated. 🎉
+
+ The current version assumes Pydantic v1, and SQLAlchemy versions less than 2.0.
+
+ The new docs will include Pydantic v2 and will use <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a> (which is also based on SQLAlchemy) once it is updated to use Pydantic v2 as well.
+
**FastAPI** doesn't require you to use a SQL (relational) database.
But you can use any relational database that you want.
from fastapi import FastAPI
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings
class Settings(BaseSettings):
class CarItem(BaseItem):
- type = "car"
+ type: str = "car"
class PlaneItem(BaseItem):
- type = "plane"
+ type: str = "plane"
size: int
class CarItem(BaseItem):
- type = "car"
+ type: str = "car"
class PlaneItem(BaseItem):
- type = "plane"
+ type: str = "plane"
size: int
"/items/",
openapi_extra={
"requestBody": {
- "content": {"application/x-yaml": {"schema": Item.schema()}},
+ "content": {"application/x-yaml": {"schema": Item.model_json_schema()}},
"required": True,
},
},
except yaml.YAMLError:
raise HTTPException(status_code=422, detail="Invalid YAML")
try:
- item = Item.parse_obj(data)
+ item = Item.model_validate(data)
except ValidationError as e:
raise HTTPException(status_code=422, detail=e.errors())
return item
--- /dev/null
+from typing import List
+
+import yaml
+from fastapi import FastAPI, HTTPException, Request
+from pydantic import BaseModel, ValidationError
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+ name: str
+ tags: List[str]
+
+
+@app.post(
+ "/items/",
+ openapi_extra={
+ "requestBody": {
+ "content": {"application/x-yaml": {"schema": Item.schema()}},
+ "required": True,
+ },
+ },
+)
+async def create_item(request: Request):
+ raw_body = await request.body()
+ try:
+ data = yaml.safe_load(raw_body)
+ except yaml.YAMLError:
+ raise HTTPException(status_code=422, detail="Invalid YAML")
+ try:
+ item = Item.parse_obj(data)
+ except ValidationError as e:
+ raise HTTPException(status_code=422, detail=e.errors())
+ return item
@app.get("/items/")
async def read_items(
q: Union[str, None] = Query(
- default=None, min_length=3, max_length=50, regex="^fixedquery$"
+ default=None, min_length=3, max_length=50, pattern="^fixedquery$"
)
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
@app.get("/items/")
async def read_items(
q: Annotated[
- Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$")
+ Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$")
] = None
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
@app.get("/items/")
async def read_items(
q: Annotated[
- str | None, Query(min_length=3, max_length=50, regex="^fixedquery$")
+ str | None, Query(min_length=3, max_length=50, pattern="^fixedquery$")
] = None
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
--- /dev/null
+from typing import Annotated
+
+from fastapi import FastAPI, Query
+
+app = FastAPI()
+
+
+@app.get("/items/")
+async def read_items(
+ q: Annotated[
+ str | None, Query(min_length=3, max_length=50, regex="^fixedquery$")
+ ] = None
+):
+ results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+ if q:
+ results.update({"q": q})
+ return results
@app.get("/items/")
async def read_items(
q: Annotated[
- Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$")
+ Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$")
] = None
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
@app.get("/items/")
async def read_items(
q: str
- | None = Query(default=None, min_length=3, max_length=50, regex="^fixedquery$")
+ | None = Query(default=None, min_length=3, max_length=50, pattern="^fixedquery$")
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
description="Query string for the items to search in the database that have a good match",
min_length=3,
max_length=50,
- regex="^fixedquery$",
+ pattern="^fixedquery$",
deprecated=True,
)
):
description="Query string for the items to search in the database that have a good match",
min_length=3,
max_length=50,
- regex="^fixedquery$",
+ pattern="^fixedquery$",
deprecated=True,
),
] = None
description="Query string for the items to search in the database that have a good match",
min_length=3,
max_length=50,
- regex="^fixedquery$",
+ pattern="^fixedquery$",
deprecated=True,
),
] = None
description="Query string for the items to search in the database that have a good match",
min_length=3,
max_length=50,
- regex="^fixedquery$",
+ pattern="^fixedquery$",
deprecated=True,
),
] = None
description="Query string for the items to search in the database that have a good match",
min_length=3,
max_length=50,
- regex="^fixedquery$",
+ pattern="^fixedquery$",
deprecated=True,
)
):
price: float
tax: Union[float, None] = None
- class Config:
- schema_extra = {
+ model_config = {
+ "json_schema_extra": {
"examples": [
{
"name": "Foo",
}
]
}
+ }
@app.put("/items/{item_id}")
--- /dev/null
+from typing import Union
+
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+ name: str
+ description: Union[str, None] = None
+ price: float
+ tax: Union[float, None] = None
+
+ class Config:
+ schema_extra = {
+ "examples": [
+ {
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ }
+ ]
+ }
+
+
+@app.put("/items/{item_id}")
+async def update_item(item_id: int, item: Item):
+ results = {"item_id": item_id, "item": item}
+ return results
price: float
tax: float | None = None
- class Config:
- schema_extra = {
+ model_config = {
+ "json_schema_extra": {
"examples": [
{
"name": "Foo",
}
]
}
+ }
@app.put("/items/{item_id}")
--- /dev/null
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+ name: str
+ description: str | None = None
+ price: float
+ tax: float | None = None
+
+ class Config:
+ schema_extra = {
+ "examples": [
+ {
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ }
+ ]
+ }
+
+
+@app.put("/items/{item_id}")
+async def update_item(item_id: int, item: Item):
+ results = {"item_id": item_id, "item": item}
+ return results
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings
class Settings(BaseSettings):
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings
class Settings(BaseSettings):
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings
class Settings(BaseSettings):
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings
class Settings(BaseSettings):
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings
class Settings(BaseSettings):
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
admin_email: str
items_per_user: int = 50
- class Config:
- env_file = ".env"
+ model_config = SettingsConfigDict(env_file=".env")
--- /dev/null
+from pydantic import BaseSettings
+
+
+class Settings(BaseSettings):
+ app_name: str = "Awesome API"
+ admin_email: str
+ items_per_user: int = 50
+
+ class Config:
+ env_file = ".env"
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings
class Settings(BaseSettings):
from fastapi import FastAPI
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings
class Settings(BaseSettings):
--- /dev/null
+from fastapi import FastAPI
+from pydantic import BaseSettings
+
+
+class Settings(BaseSettings):
+ app_name: str = "Awesome API"
+ admin_email: str
+ items_per_user: int = 50
+
+
+settings = Settings()
+app = FastAPI()
+
+
+@app.get("/info")
+async def info():
+ return {
+ "app_name": settings.app_name,
+ "admin_email": settings.admin_email,
+ "items_per_user": settings.items_per_user,
+ }
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
-__version__ = "0.99.1"
+__version__ = "0.100.0-beta3"
from starlette import status as status
--- /dev/null
+from collections import deque
+from copy import copy
+from dataclasses import dataclass, is_dataclass
+from enum import Enum
+from typing import (
+ Any,
+ Callable,
+ Deque,
+ Dict,
+ FrozenSet,
+ List,
+ Mapping,
+ Sequence,
+ Set,
+ Tuple,
+ Type,
+ Union,
+)
+
+from fastapi.exceptions import RequestErrorModel
+from fastapi.types import IncEx, ModelNameMap, UnionType
+from pydantic import BaseModel, create_model
+from pydantic.version import VERSION as PYDANTIC_VERSION
+from starlette.datastructures import UploadFile
+from typing_extensions import Annotated, Literal, get_args, get_origin
+
+PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.")
+
+
+sequence_annotation_to_type = {
+ Sequence: list,
+ List: list,
+ list: list,
+ Tuple: tuple,
+ tuple: tuple,
+ Set: set,
+ set: set,
+ FrozenSet: frozenset,
+ frozenset: frozenset,
+ Deque: deque,
+ deque: deque,
+}
+
+sequence_types = tuple(sequence_annotation_to_type.keys())
+
+if PYDANTIC_V2:
+ from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
+ from pydantic import TypeAdapter
+ from pydantic import ValidationError as ValidationError
+ from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined]
+ GetJsonSchemaHandler as GetJsonSchemaHandler,
+ )
+ from pydantic._internal._typing_extra import eval_type_lenient
+ from pydantic._internal._utils import lenient_issubclass as lenient_issubclass
+ from pydantic.fields import FieldInfo
+ from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema
+ from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue
+ from pydantic_core import CoreSchema as CoreSchema
+ from pydantic_core import MultiHostUrl as MultiHostUrl
+ from pydantic_core import PydanticUndefined, PydanticUndefinedType
+ from pydantic_core import Url as Url
+ from pydantic_core.core_schema import (
+ general_plain_validator_function as general_plain_validator_function,
+ )
+
+ Required = PydanticUndefined
+ Undefined = PydanticUndefined
+ UndefinedType = PydanticUndefinedType
+ evaluate_forwardref = eval_type_lenient
+ Validator = Any
+
+ class BaseConfig:
+ pass
+
+ class ErrorWrapper(Exception):
+ pass
+
+ @dataclass
+ class ModelField:
+ field_info: FieldInfo
+ name: str
+ mode: Literal["validation", "serialization"] = "validation"
+
+ @property
+ def alias(self) -> str:
+ a = self.field_info.alias
+ return a if a is not None else self.name
+
+ @property
+ def required(self) -> bool:
+ return self.field_info.is_required()
+
+ @property
+ def default(self) -> Any:
+ return self.get_default()
+
+ @property
+ def type_(self) -> Any:
+ return self.field_info.annotation
+
+ def __post_init__(self) -> None:
+ self._type_adapter: TypeAdapter[Any] = TypeAdapter(
+ Annotated[self.field_info.annotation, self.field_info]
+ )
+
+ def get_default(self) -> Any:
+ if self.field_info.is_required():
+ return Undefined
+ return self.field_info.get_default(call_default_factory=True)
+
+ def validate(
+ self,
+ value: Any,
+ values: Dict[str, Any] = {}, # noqa: B006
+ *,
+ loc: Tuple[Union[int, str], ...] = (),
+ ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]:
+ try:
+ return (
+ self._type_adapter.validate_python(value, from_attributes=True),
+ None,
+ )
+ except ValidationError as exc:
+ return None, _regenerate_error_with_loc(
+ errors=exc.errors(), loc_prefix=loc
+ )
+
+ def serialize(
+ self,
+ value: Any,
+ *,
+ mode: Literal["json", "python"] = "json",
+ include: Union[IncEx, None] = None,
+ exclude: Union[IncEx, None] = None,
+ by_alias: bool = True,
+ exclude_unset: bool = False,
+ exclude_defaults: bool = False,
+ exclude_none: bool = False,
+ ) -> Any:
+ # What calls this code passes a value that already called
+ # self._type_adapter.validate_python(value)
+ return self._type_adapter.dump_python(
+ value,
+ mode=mode,
+ include=include,
+ exclude=exclude,
+ by_alias=by_alias,
+ exclude_unset=exclude_unset,
+ exclude_defaults=exclude_defaults,
+ exclude_none=exclude_none,
+ )
+
+ def __hash__(self) -> int:
+ # Each ModelField is unique for our purposes, to allow making a dict from
+ # ModelField to its JSON Schema.
+ return id(self)
+
+ def get_annotation_from_field_info(
+ annotation: Any, field_info: FieldInfo, field_name: str
+ ) -> Any:
+ return annotation
+
+ def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
+ return errors # type: ignore[return-value]
+
+ def _model_rebuild(model: Type[BaseModel]) -> None:
+ model.model_rebuild()
+
+ def _model_dump(
+ model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
+ ) -> Any:
+ return model.model_dump(mode=mode, **kwargs)
+
+ def _get_model_config(model: BaseModel) -> Any:
+ return model.model_config
+
+ def get_schema_from_model_field(
+ *,
+ field: ModelField,
+ schema_generator: GenerateJsonSchema,
+ model_name_map: ModelNameMap,
+ field_mapping: Dict[
+ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+ ],
+ ) -> Dict[str, Any]:
+ # This expects that GenerateJsonSchema was already used to generate the definitions
+ json_schema = field_mapping[(field, field.mode)]
+ if "$ref" not in json_schema:
+ # TODO remove when deprecating Pydantic v1
+ # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207
+ json_schema[
+ "title"
+ ] = field.field_info.title or field.alias.title().replace("_", " ")
+ return json_schema
+
+ def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
+ return {}
+
+ def get_definitions(
+ *,
+ fields: List[ModelField],
+ schema_generator: GenerateJsonSchema,
+ model_name_map: ModelNameMap,
+ ) -> Tuple[
+ Dict[
+ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+ ],
+ Dict[str, Dict[str, Any]],
+ ]:
+ inputs = [
+ (field, field.mode, field._type_adapter.core_schema) for field in fields
+ ]
+ field_mapping, definitions = schema_generator.generate_definitions(
+ inputs=inputs
+ )
+ return field_mapping, definitions # type: ignore[return-value]
+
+ def is_scalar_field(field: ModelField) -> bool:
+ from fastapi import params
+
+ return field_annotation_is_scalar(
+ field.field_info.annotation
+ ) and not isinstance(field.field_info, params.Body)
+
+ def is_sequence_field(field: ModelField) -> bool:
+ return field_annotation_is_sequence(field.field_info.annotation)
+
+ def is_scalar_sequence_field(field: ModelField) -> bool:
+ return field_annotation_is_scalar_sequence(field.field_info.annotation)
+
+ def is_bytes_field(field: ModelField) -> bool:
+ return is_bytes_or_nonable_bytes_annotation(field.type_)
+
+ def is_bytes_sequence_field(field: ModelField) -> bool:
+ return is_bytes_sequence_annotation(field.type_)
+
+ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
+ return type(field_info).from_annotation(annotation)
+
+ def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
+ origin_type = (
+ get_origin(field.field_info.annotation) or field.field_info.annotation
+ )
+ assert issubclass(origin_type, sequence_types) # type: ignore[arg-type]
+ return sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return]
+
+ def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
+ error = ValidationError.from_exception_data(
+ "Field required", [{"type": "missing", "loc": loc, "input": {}}]
+ ).errors()[0]
+ error["input"] = None
+ return error # type: ignore[return-value]
+
+ def create_body_model(
+ *, fields: Sequence[ModelField], model_name: str
+ ) -> Type[BaseModel]:
+ field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields}
+ BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload]
+ return BodyModel
+
+else:
+ from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX
+ from pydantic import AnyUrl as Url # noqa: F401
+ from pydantic import ( # type: ignore[assignment]
+ BaseConfig as BaseConfig, # noqa: F401
+ )
+ from pydantic import ValidationError as ValidationError # noqa: F401
+ from pydantic.class_validators import ( # type: ignore[no-redef]
+ Validator as Validator, # noqa: F401
+ )
+ from pydantic.error_wrappers import ( # type: ignore[no-redef]
+ ErrorWrapper as ErrorWrapper, # noqa: F401
+ )
+ from pydantic.errors import MissingError
+ from pydantic.fields import ( # type: ignore[attr-defined]
+ SHAPE_FROZENSET,
+ SHAPE_LIST,
+ SHAPE_SEQUENCE,
+ SHAPE_SET,
+ SHAPE_SINGLETON,
+ SHAPE_TUPLE,
+ SHAPE_TUPLE_ELLIPSIS,
+ )
+ from pydantic.fields import FieldInfo as FieldInfo
+ from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
+ ModelField as ModelField, # noqa: F401
+ )
+ from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
+ Required as Required, # noqa: F401
+ )
+ from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
+ Undefined as Undefined,
+ )
+ from pydantic.fields import ( # type: ignore[no-redef, attr-defined]
+ UndefinedType as UndefinedType, # noqa: F401
+ )
+ from pydantic.networks import ( # type: ignore[no-redef]
+ MultiHostDsn as MultiHostUrl, # noqa: F401
+ )
+ from pydantic.schema import (
+ field_schema,
+ get_flat_models_from_fields,
+ get_model_name_map,
+ model_process_schema,
+ )
+ from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401
+ get_annotation_from_field_info as get_annotation_from_field_info,
+ )
+ from pydantic.typing import ( # type: ignore[no-redef]
+ evaluate_forwardref as evaluate_forwardref, # noqa: F401
+ )
+ from pydantic.utils import ( # type: ignore[no-redef]
+ lenient_issubclass as lenient_issubclass, # noqa: F401
+ )
+
+ GetJsonSchemaHandler = Any # type: ignore[assignment,misc]
+ JsonSchemaValue = Dict[str, Any] # type: ignore[misc]
+ CoreSchema = Any # type: ignore[assignment,misc]
+
+ sequence_shapes = {
+ SHAPE_LIST,
+ SHAPE_SET,
+ SHAPE_FROZENSET,
+ SHAPE_TUPLE,
+ SHAPE_SEQUENCE,
+ SHAPE_TUPLE_ELLIPSIS,
+ }
+ sequence_shape_to_type = {
+ SHAPE_LIST: list,
+ SHAPE_SET: set,
+ SHAPE_TUPLE: tuple,
+ SHAPE_SEQUENCE: list,
+ SHAPE_TUPLE_ELLIPSIS: list,
+ }
+
+ @dataclass
+ class GenerateJsonSchema: # type: ignore[no-redef]
+ ref_template: str
+
+ class PydanticSchemaGenerationError(Exception): # type: ignore[no-redef]
+ pass
+
+ def general_plain_validator_function( # type: ignore[misc]
+ function: Callable[..., Any],
+ *,
+ ref: Union[str, None] = None,
+ metadata: Any = None,
+ serialization: Any = None,
+ ) -> Any:
+ return {}
+
+ def get_model_definitions(
+ *,
+ flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
+ model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
+ ) -> Dict[str, Any]:
+ definitions: Dict[str, Dict[str, Any]] = {}
+ for model in flat_models:
+ m_schema, m_definitions, m_nested_models = model_process_schema(
+ model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
+ )
+ definitions.update(m_definitions)
+ model_name = model_name_map[model]
+ if "description" in m_schema:
+ m_schema["description"] = m_schema["description"].split("\f")[0]
+ definitions[model_name] = m_schema
+ return definitions
+
+ def is_pv1_scalar_field(field: ModelField) -> bool:
+ from fastapi import params
+
+ field_info = field.field_info
+ if not (
+ field.shape == SHAPE_SINGLETON # type: ignore[attr-defined]
+ and not lenient_issubclass(field.type_, BaseModel)
+ and not lenient_issubclass(field.type_, dict)
+ and not field_annotation_is_sequence(field.type_)
+ and not is_dataclass(field.type_)
+ and not isinstance(field_info, params.Body)
+ ):
+ return False
+ if field.sub_fields: # type: ignore[attr-defined]
+ if not all(
+ is_pv1_scalar_field(f)
+ for f in field.sub_fields # type: ignore[attr-defined]
+ ):
+ return False
+ return True
+
+ def is_pv1_scalar_sequence_field(field: ModelField) -> bool:
+ if (field.shape in sequence_shapes) and not lenient_issubclass( # type: ignore[attr-defined]
+ field.type_, BaseModel
+ ):
+ if field.sub_fields is not None: # type: ignore[attr-defined]
+ for sub_field in field.sub_fields: # type: ignore[attr-defined]
+ if not is_pv1_scalar_field(sub_field):
+ return False
+ return True
+ if _annotation_is_sequence(field.type_):
+ return True
+ return False
+
+ def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
+ use_errors: List[Any] = []
+ for error in errors:
+ if isinstance(error, ErrorWrapper):
+ new_errors = ValidationError( # type: ignore[call-arg]
+ errors=[error], model=RequestErrorModel
+ ).errors()
+ use_errors.extend(new_errors)
+ elif isinstance(error, list):
+ use_errors.extend(_normalize_errors(error))
+ else:
+ use_errors.append(error)
+ return use_errors
+
+ def _model_rebuild(model: Type[BaseModel]) -> None:
+ model.update_forward_refs()
+
+ def _model_dump(
+ model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
+ ) -> Any:
+ return model.dict(**kwargs)
+
+ def _get_model_config(model: BaseModel) -> Any:
+ return model.__config__ # type: ignore[attr-defined]
+
+ def get_schema_from_model_field(
+ *,
+ field: ModelField,
+ schema_generator: GenerateJsonSchema,
+ model_name_map: ModelNameMap,
+ field_mapping: Dict[
+ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+ ],
+ ) -> Dict[str, Any]:
+ # This expects that GenerateJsonSchema was already used to generate the definitions
+ return field_schema( # type: ignore[no-any-return]
+ field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
+ )[0]
+
+ def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
+ models = get_flat_models_from_fields(fields, known_models=set())
+ return get_model_name_map(models) # type: ignore[no-any-return]
+
+ def get_definitions(
+ *,
+ fields: List[ModelField],
+ schema_generator: GenerateJsonSchema,
+ model_name_map: ModelNameMap,
+ ) -> Tuple[
+ Dict[
+ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+ ],
+ Dict[str, Dict[str, Any]],
+ ]:
+ models = get_flat_models_from_fields(fields, known_models=set())
+ return {}, get_model_definitions(
+ flat_models=models, model_name_map=model_name_map
+ )
+
+ def is_scalar_field(field: ModelField) -> bool:
+ return is_pv1_scalar_field(field)
+
+ def is_sequence_field(field: ModelField) -> bool:
+ return field.shape in sequence_shapes or _annotation_is_sequence(field.type_) # type: ignore[attr-defined]
+
+ def is_scalar_sequence_field(field: ModelField) -> bool:
+ return is_pv1_scalar_sequence_field(field)
+
+ def is_bytes_field(field: ModelField) -> bool:
+ return lenient_issubclass(field.type_, bytes)
+
+ def is_bytes_sequence_field(field: ModelField) -> bool:
+ return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) # type: ignore[attr-defined]
+
+ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
+ return copy(field_info)
+
+ def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
+ return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return,attr-defined]
+
+ def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
+ missing_field_error = ErrorWrapper(MissingError(), loc=loc) # type: ignore[call-arg]
+ new_error = ValidationError([missing_field_error], RequestErrorModel)
+ return new_error.errors()[0] # type: ignore[return-value]
+
+ def create_body_model(
+ *, fields: Sequence[ModelField], model_name: str
+ ) -> Type[BaseModel]:
+ BodyModel = create_model(model_name)
+ for f in fields:
+ BodyModel.__fields__[f.name] = f # type: ignore[index]
+ return BodyModel
+
+
+def _regenerate_error_with_loc(
+ *, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
+) -> List[Dict[str, Any]]:
+ updated_loc_errors: List[Any] = [
+ {**err, "loc": loc_prefix + err.get("loc", ())}
+ for err in _normalize_errors(errors)
+ ]
+
+ return updated_loc_errors
+
+
+def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
+ if lenient_issubclass(annotation, (str, bytes)):
+ return False
+ return lenient_issubclass(annotation, sequence_types)
+
+
+def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
+ return _annotation_is_sequence(annotation) or _annotation_is_sequence(
+ get_origin(annotation)
+ )
+
+
+def value_is_sequence(value: Any) -> bool:
+ return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type]
+
+
+def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
+ return (
+ lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile))
+ or _annotation_is_sequence(annotation)
+ or is_dataclass(annotation)
+ )
+
+
+def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
+ origin = get_origin(annotation)
+ if origin is Union or origin is UnionType:
+ return any(field_annotation_is_complex(arg) for arg in get_args(annotation))
+
+ return (
+ _annotation_is_complex(annotation)
+ or _annotation_is_complex(origin)
+ or hasattr(origin, "__pydantic_core_schema__")
+ or hasattr(origin, "__get_pydantic_core_schema__")
+ )
+
+
+def field_annotation_is_scalar(annotation: Any) -> bool:
+ # handle Ellipsis here to make tuple[int, ...] work nicely
+ return annotation is Ellipsis or not field_annotation_is_complex(annotation)
+
+
+def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool:
+ origin = get_origin(annotation)
+ if origin is Union or origin is UnionType:
+ at_least_one_scalar_sequence = False
+ for arg in get_args(annotation):
+ if field_annotation_is_scalar_sequence(arg):
+ at_least_one_scalar_sequence = True
+ continue
+ elif not field_annotation_is_scalar(arg):
+ return False
+ return at_least_one_scalar_sequence
+ return field_annotation_is_sequence(annotation) and all(
+ field_annotation_is_scalar(sub_annotation)
+ for sub_annotation in get_args(annotation)
+ )
+
+
+def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool:
+ if lenient_issubclass(annotation, bytes):
+ return True
+ origin = get_origin(annotation)
+ if origin is Union or origin is UnionType:
+ for arg in get_args(annotation):
+ if lenient_issubclass(arg, bytes):
+ return True
+ return False
+
+
+def is_uploadfile_or_nonable_uploadfile_annotation(annotation: Any) -> bool:
+ if lenient_issubclass(annotation, UploadFile):
+ return True
+ origin = get_origin(annotation)
+ if origin is Union or origin is UnionType:
+ for arg in get_args(annotation):
+ if lenient_issubclass(arg, UploadFile):
+ return True
+ return False
+
+
+def is_bytes_sequence_annotation(annotation: Any) -> bool:
+ origin = get_origin(annotation)
+ if origin is Union or origin is UnionType:
+ at_least_one = False
+ for arg in get_args(annotation):
+ if is_bytes_sequence_annotation(arg):
+ at_least_one = True
+ continue
+ return at_least_one
+ return field_annotation_is_sequence(annotation) and all(
+ is_bytes_or_nonable_bytes_annotation(sub_annotation)
+ for sub_annotation in get_args(annotation)
+ )
+
+
+def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
+ origin = get_origin(annotation)
+ if origin is Union or origin is UnionType:
+ at_least_one = False
+ for arg in get_args(annotation):
+ if is_uploadfile_sequence_annotation(arg):
+ at_least_one = True
+ continue
+ return at_least_one
+ return field_annotation_is_sequence(annotation) and all(
+ is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation)
+ for sub_annotation in get_args(annotation)
+ )
from fastapi import routing
from fastapi.datastructures import Default, DefaultPlaceholder
-from fastapi.encoders import DictIntStrAny, SetIntStr
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
)
from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends
-from fastapi.types import DecoratedCallable
+from fastapi.types import DecoratedCallable, IncEx
from fastapi.utils import generate_unique_id
from starlette.applications import Starlette
from starlette.datastructures import State
deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
-from typing import Any, Callable, Dict, Iterable, Type, TypeVar
-
+from typing import Any, Callable, Dict, Iterable, Type, TypeVar, cast
+
+from fastapi._compat import (
+ PYDANTIC_V2,
+ CoreSchema,
+ GetJsonSchemaHandler,
+ JsonSchemaValue,
+ general_plain_validator_function,
+)
from starlette.datastructures import URL as URL # noqa: F401
from starlette.datastructures import Address as Address # noqa: F401
from starlette.datastructures import FormData as FormData # noqa: F401
return v
@classmethod
- def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
- field_schema.update({"type": "string", "format": "binary"})
+ def _validate(cls, __input_value: Any, _: Any) -> "UploadFile":
+ if not isinstance(__input_value, StarletteUploadFile):
+ raise ValueError(f"Expected UploadFile, received: {type(__input_value)}")
+ return cast(UploadFile, __input_value)
+
+ if not PYDANTIC_V2:
+
+ @classmethod
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
+ field_schema.update({"type": "string", "format": "binary"})
+
+ @classmethod
+ def __get_pydantic_json_schema__(
+ cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
+ ) -> JsonSchemaValue:
+ return {"type": "string", "format": "binary"}
+
+ @classmethod
+ def __get_pydantic_core_schema__(
+ cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
+ ) -> CoreSchema:
+ return general_plain_validator_function(cls._validate)
class DefaultPlaceholder:
from typing import Any, Callable, List, Optional, Sequence
+from fastapi._compat import ModelField
from fastapi.security.base import SecurityBase
-from pydantic.fields import ModelField
class SecurityRequirement:
-import dataclasses
import inspect
from contextlib import contextmanager
-from copy import copy, deepcopy
+from copy import deepcopy
from typing import (
Any,
Callable,
import anyio
from fastapi import params
+from fastapi._compat import (
+ PYDANTIC_V2,
+ ErrorWrapper,
+ ModelField,
+ Required,
+ Undefined,
+ _regenerate_error_with_loc,
+ copy_field_info,
+ create_body_model,
+ evaluate_forwardref,
+ field_annotation_is_scalar,
+ get_annotation_from_field_info,
+ get_missing_field_error,
+ is_bytes_field,
+ is_bytes_sequence_field,
+ is_scalar_field,
+ is_scalar_sequence_field,
+ is_sequence_field,
+ is_uploadfile_or_nonable_uploadfile_annotation,
+ is_uploadfile_sequence_annotation,
+ lenient_issubclass,
+ sequence_types,
+ serialize_sequence_value,
+ value_is_sequence,
+)
from fastapi.concurrency import (
AsyncExitStack,
asynccontextmanager,
from fastapi.security.oauth2 import OAuth2, SecurityScopes
from fastapi.security.open_id_connect_url import OpenIdConnect
from fastapi.utils import create_response_field, get_path_param_names
-from pydantic import BaseModel, create_model
-from pydantic.error_wrappers import ErrorWrapper
-from pydantic.errors import MissingError
-from pydantic.fields import (
- SHAPE_FROZENSET,
- SHAPE_LIST,
- SHAPE_SEQUENCE,
- SHAPE_SET,
- SHAPE_SINGLETON,
- SHAPE_TUPLE,
- SHAPE_TUPLE_ELLIPSIS,
- FieldInfo,
- ModelField,
- Required,
- Undefined,
-)
-from pydantic.schema import get_annotation_from_field_info
-from pydantic.typing import evaluate_forwardref, get_args, get_origin
-from pydantic.utils import lenient_issubclass
+from pydantic.fields import FieldInfo
from starlette.background import BackgroundTasks
from starlette.concurrency import run_in_threadpool
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
from starlette.requests import HTTPConnection, Request
from starlette.responses import Response
from starlette.websockets import WebSocket
-from typing_extensions import Annotated
-
-sequence_shapes = {
- SHAPE_LIST,
- SHAPE_SET,
- SHAPE_FROZENSET,
- SHAPE_TUPLE,
- SHAPE_SEQUENCE,
- SHAPE_TUPLE_ELLIPSIS,
-}
-sequence_types = (list, set, tuple)
-sequence_shape_to_type = {
- SHAPE_LIST: list,
- SHAPE_SET: set,
- SHAPE_TUPLE: tuple,
- SHAPE_SEQUENCE: list,
- SHAPE_TUPLE_ELLIPSIS: list,
-}
-
+from typing_extensions import Annotated, get_args, get_origin
multipart_not_installed_error = (
'Form data requires "python-multipart" to be installed. \n'
)
-def is_scalar_field(field: ModelField) -> bool:
- field_info = field.field_info
- if not (
- field.shape == SHAPE_SINGLETON
- and not lenient_issubclass(field.type_, BaseModel)
- and not lenient_issubclass(field.type_, sequence_types + (dict,))
- and not dataclasses.is_dataclass(field.type_)
- and not isinstance(field_info, params.Body)
- ):
- return False
- if field.sub_fields:
- if not all(is_scalar_field(f) for f in field.sub_fields):
- return False
- return True
-
-
-def is_scalar_sequence_field(field: ModelField) -> bool:
- if (field.shape in sequence_shapes) and not lenient_issubclass(
- field.type_, BaseModel
- ):
- if field.sub_fields is not None:
- for sub_field in field.sub_fields:
- if not is_scalar_field(sub_field):
- return False
- return True
- if lenient_issubclass(field.type_, sequence_types):
- return True
- return False
-
-
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
signature = inspect.signature(call)
globalns = getattr(call, "__globals__", {})
is_path_param: bool,
) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]:
field_info = None
- used_default_field_info = False
depends = None
type_annotation: Any = Any
if (
annotation is not inspect.Signature.empty
- and get_origin(annotation) is Annotated # type: ignore[comparison-overlap]
+ and get_origin(annotation) is Annotated
):
annotated_args = get_args(annotation)
type_annotation = annotated_args[0]
fastapi_annotation = next(iter(fastapi_annotations), None)
if isinstance(fastapi_annotation, FieldInfo):
# Copy `field_info` because we mutate `field_info.default` below.
- field_info = copy(fastapi_annotation)
+ field_info = copy_field_info(
+ field_info=fastapi_annotation, annotation=annotation
+ )
assert field_info.default is Undefined or field_info.default is Required, (
f"`{field_info.__class__.__name__}` default value cannot be set in"
f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
f" together for {param_name!r}"
)
field_info = value
+ if PYDANTIC_V2:
+ field_info.annotation = type_annotation
if depends is not None and depends.dependency is None:
depends.dependency = type_annotation
# We might check here that `default_value is Required`, but the fact is that the same
# parameter might sometimes be a path parameter and sometimes not. See
# `tests/test_infer_param_optionality.py` for an example.
- field_info = params.Path()
+ field_info = params.Path(annotation=type_annotation)
+ elif is_uploadfile_or_nonable_uploadfile_annotation(
+ type_annotation
+ ) or is_uploadfile_sequence_annotation(type_annotation):
+ field_info = params.File(annotation=type_annotation, default=default_value)
+ elif not field_annotation_is_scalar(annotation=type_annotation):
+ field_info = params.Body(annotation=type_annotation, default=default_value)
else:
- field_info = params.Query(default=default_value)
- used_default_field_info = True
+ field_info = params.Query(annotation=type_annotation, default=default_value)
field = None
if field_info is not None:
and getattr(field_info, "in_", None) is None
):
field_info.in_ = params.ParamTypes.query
- annotation = get_annotation_from_field_info(
- annotation if annotation is not inspect.Signature.empty else Any,
+ use_annotation = get_annotation_from_field_info(
+ type_annotation,
field_info,
param_name,
)
alias = param_name.replace("_", "-")
else:
alias = field_info.alias or param_name
+ field_info.alias = alias
field = create_response_field(
name=param_name,
- type_=annotation,
+ type_=use_annotation,
default=field_info.default,
alias=alias,
required=field_info.default in (Required, Undefined),
field_info=field_info,
)
- if used_default_field_info:
- if lenient_issubclass(field.type_, UploadFile):
- field.field_info = params.File(field_info.default)
- elif not is_scalar_field(field=field):
- field.field_info = params.Body(field_info.default)
return type_annotation, depends, field
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
) -> Tuple[
Dict[str, Any],
- List[ErrorWrapper],
+ List[Any],
Optional[BackgroundTasks],
Response,
Dict[Tuple[Callable[..., Any], Tuple[str]], Any],
]:
values: Dict[str, Any] = {}
- errors: List[ErrorWrapper] = []
+ errors: List[Any] = []
if response is None:
response = Response()
del response.headers["content-length"]
def request_params_to_args(
required_params: Sequence[ModelField],
received_params: Union[Mapping[str, Any], QueryParams, Headers],
-) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
+) -> Tuple[Dict[str, Any], List[Any]]:
values = {}
errors = []
for field in required_params:
assert isinstance(
field_info, params.Param
), "Params must be subclasses of Param"
+ loc = (field_info.in_.value, field.alias)
if value is None:
if field.required:
- errors.append(
- ErrorWrapper(
- MissingError(), loc=(field_info.in_.value, field.alias)
- )
- )
+ errors.append(get_missing_field_error(loc=loc))
else:
values[field.name] = deepcopy(field.default)
continue
- v_, errors_ = field.validate(
- value, values, loc=(field_info.in_.value, field.alias)
- )
+ v_, errors_ = field.validate(value, values, loc=loc)
if isinstance(errors_, ErrorWrapper):
errors.append(errors_)
elif isinstance(errors_, list):
- errors.extend(errors_)
+ new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
+ errors.extend(new_errors)
else:
values[field.name] = v_
return values, errors
async def request_body_to_args(
required_params: List[ModelField],
received_body: Optional[Union[Dict[str, Any], FormData]],
-) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
+) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
values = {}
- errors = []
+ errors: List[Dict[str, Any]] = []
if required_params:
field = required_params[0]
field_info = field.field_info
value: Optional[Any] = None
if received_body is not None:
- if (
- field.shape in sequence_shapes or field.type_ in sequence_types
- ) and isinstance(received_body, FormData):
+ if (is_sequence_field(field)) and isinstance(received_body, FormData):
value = received_body.getlist(field.alias)
else:
try:
or (isinstance(field_info, params.Form) and value == "")
or (
isinstance(field_info, params.Form)
- and field.shape in sequence_shapes
+ and is_sequence_field(field)
and len(value) == 0
)
):
continue
if (
isinstance(field_info, params.File)
- and lenient_issubclass(field.type_, bytes)
+ and is_bytes_field(field)
and isinstance(value, UploadFile)
):
value = await value.read()
elif (
- field.shape in sequence_shapes
+ is_bytes_sequence_field(field)
and isinstance(field_info, params.File)
- and lenient_issubclass(field.type_, bytes)
- and isinstance(value, sequence_types)
+ and value_is_sequence(value)
):
+ # For types
+ assert isinstance(value, sequence_types) # type: ignore[arg-type]
results: List[Union[bytes, str]] = []
async def process_fn(
async with anyio.create_task_group() as tg:
for sub_value in value:
tg.start_soon(process_fn, sub_value.read)
- value = sequence_shape_to_type[field.shape](results)
+ value = serialize_sequence_value(field=field, value=results)
v_, errors_ = field.validate(value, values, loc=loc)
- if isinstance(errors_, ErrorWrapper):
- errors.append(errors_)
- elif isinstance(errors_, list):
+ if isinstance(errors_, list):
errors.extend(errors_)
+ elif errors_:
+ errors.append(errors_)
else:
values[field.name] = v_
return values, errors
-def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper:
- missing_field_error = ErrorWrapper(MissingError(), loc=loc)
- return missing_field_error
-
-
def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
flat_dependant = get_flat_dependant(dependant)
if not flat_dependant.body_params:
for param in flat_dependant.body_params:
setattr(param.field_info, "embed", True) # noqa: B010
model_name = "Body_" + name
- BodyModel: Type[BaseModel] = create_model(model_name)
- for f in flat_dependant.body_params:
- BodyModel.__fields__[f.name] = f
+ BodyModel = create_body_model(
+ fields=flat_dependant.body_params, model_name=model_name
+ )
required = any(True for f in flat_dependant.body_params if f.required)
-
- BodyFieldInfo_kwargs: Dict[str, Any] = {"default": None}
+ BodyFieldInfo_kwargs: Dict[str, Any] = {
+ "annotation": BodyModel,
+ "alias": "body",
+ }
+ if not required:
+ BodyFieldInfo_kwargs["default"] = None
if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params):
BodyFieldInfo: Type[params.Body] = params.File
elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params):
import dataclasses
+import datetime
from collections import defaultdict, deque
+from decimal import Decimal
from enum import Enum
-from pathlib import PurePath
+from ipaddress import (
+ IPv4Address,
+ IPv4Interface,
+ IPv4Network,
+ IPv6Address,
+ IPv6Interface,
+ IPv6Network,
+)
+from pathlib import Path, PurePath
+from re import Pattern
from types import GeneratorType
-from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
+from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
+from uuid import UUID
+from fastapi.types import IncEx
from pydantic import BaseModel
-from pydantic.json import ENCODERS_BY_TYPE
+from pydantic.color import Color
+from pydantic.networks import NameEmail
+from pydantic.types import SecretBytes, SecretStr
-SetIntStr = Set[Union[int, str]]
-DictIntStrAny = Dict[Union[int, str], Any]
+from ._compat import PYDANTIC_V2, MultiHostUrl, Url, _model_dump
+
+
+# Taken from Pydantic v1 as is
+def isoformat(o: Union[datetime.date, datetime.time]) -> str:
+ return o.isoformat()
+
+
+# Taken from Pydantic v1 as is
+# TODO: pv2 should this return strings instead?
+def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
+ """
+ Encodes a Decimal as int of there's no exponent, otherwise float
+
+ This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
+ where a integer (but not int typed) is used. Encoding this as a float
+ results in failed round-tripping between encode and parse.
+ Our Id type is a prime example of this.
+
+ >>> decimal_encoder(Decimal("1.0"))
+ 1.0
+
+ >>> decimal_encoder(Decimal("1"))
+ 1
+ """
+ if dec_value.as_tuple().exponent >= 0: # type: ignore[operator]
+ return int(dec_value)
+ else:
+ return float(dec_value)
+
+
+ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
+ bytes: lambda o: o.decode(),
+ Color: str,
+ datetime.date: isoformat,
+ datetime.datetime: isoformat,
+ datetime.time: isoformat,
+ datetime.timedelta: lambda td: td.total_seconds(),
+ Decimal: decimal_encoder,
+ Enum: lambda o: o.value,
+ frozenset: list,
+ deque: list,
+ GeneratorType: list,
+ IPv4Address: str,
+ IPv4Interface: str,
+ IPv4Network: str,
+ IPv6Address: str,
+ IPv6Interface: str,
+ IPv6Network: str,
+ NameEmail: str,
+ Path: str,
+ Pattern: lambda o: o.pattern,
+ SecretBytes: str,
+ SecretStr: str,
+ set: list,
+ UUID: str,
+ Url: str,
+ MultiHostUrl: str,
+}
def generate_encoders_by_class_tuples(
def jsonable_encoder(
obj: Any,
- include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ include: Optional[IncEx] = None,
+ exclude: Optional[IncEx] = None,
by_alias: bool = True,
exclude_unset: bool = False,
exclude_defaults: bool = False,
if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude)
if isinstance(obj, BaseModel):
- encoder = getattr(obj.__config__, "json_encoders", {})
- if custom_encoder:
- encoder.update(custom_encoder)
- obj_dict = obj.dict(
+ # TODO: remove when deprecating Pydantic v1
+ encoders: Dict[Any, Any] = {}
+ if not PYDANTIC_V2:
+ encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined]
+ if custom_encoder:
+ encoders.update(custom_encoder)
+ obj_dict = _model_dump(
+ obj,
+ mode="json",
include=include,
exclude=exclude,
by_alias=by_alias,
obj_dict,
exclude_none=exclude_none,
exclude_defaults=exclude_defaults,
- custom_encoder=encoder,
+ # TODO: remove when deprecating Pydantic v1
+ custom_encoder=encoders,
sqlalchemy_safe=sqlalchemy_safe,
)
if dataclasses.is_dataclass(obj):
from typing import Any, Dict, Optional, Sequence, Type
-from pydantic import BaseModel, ValidationError, create_model
-from pydantic.error_wrappers import ErrorList
+from pydantic import BaseModel, create_model
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.exceptions import WebSocketException as WebSocketException # noqa: F401
"""
-class RequestValidationError(ValidationError):
- def __init__(self, errors: Sequence[ErrorList], *, body: Any = None) -> None:
+class ValidationException(Exception):
+ def __init__(self, errors: Sequence[Any]) -> None:
+ self._errors = errors
+
+ def errors(self) -> Sequence[Any]:
+ return self._errors
+
+
+class RequestValidationError(ValidationException):
+ def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
+ super().__init__(errors)
self.body = body
- super().__init__(errors, RequestErrorModel)
-class WebSocketRequestValidationError(ValidationError):
- def __init__(self, errors: Sequence[ErrorList]) -> None:
- super().__init__(errors, WebSocketErrorModel)
+class WebSocketRequestValidationError(ValidationException):
+ pass
+
+
+class ResponseValidationError(ValidationException):
+ def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
+ super().__init__(errors)
+ self.body = body
METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"}
REF_PREFIX = "#/components/schemas/"
+REF_TEMPLATE = "#/components/schemas/{model}"
from enum import Enum
-from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Union
-
+from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Type, Union
+
+from fastapi._compat import (
+ PYDANTIC_V2,
+ CoreSchema,
+ GetJsonSchemaHandler,
+ JsonSchemaValue,
+ _model_rebuild,
+ general_plain_validator_function,
+)
from fastapi.logger import logger
from pydantic import AnyUrl, BaseModel, Field
from typing_extensions import Annotated, Literal
from typing_extensions import deprecated as typing_deprecated
try:
- import email_validator # type: ignore
+ import email_validator
assert email_validator # make autoflake ignore the unused import
from pydantic import EmailStr
)
return str(v)
+ @classmethod
+ def _validate(cls, __input_value: Any, _: Any) -> str:
+ logger.warning(
+ "email-validator not installed, email fields will be treated as str.\n"
+ "To install, run: pip install email-validator"
+ )
+ return str(__input_value)
+
+ @classmethod
+ def __get_pydantic_json_schema__(
+ cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
+ ) -> JsonSchemaValue:
+ return {"type": "string", "format": "email"}
+
+ @classmethod
+ def __get_pydantic_core_schema__(
+ cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
+ ) -> CoreSchema:
+ return general_plain_validator_function(cls._validate)
+
class Contact(BaseModel):
name: Optional[str] = None
url: Optional[AnyUrl] = None
email: Optional[EmailStr] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class License(BaseModel):
identifier: Optional[str] = None
url: Optional[AnyUrl] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class Info(BaseModel):
license: Optional[License] = None
version: str
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class ServerVariable(BaseModel):
- enum: Annotated[Optional[List[str]], Field(min_items=1)] = None
+ enum: Annotated[Optional[List[str]], Field(min_length=1)] = None
default: str
description: Optional[str] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class Server(BaseModel):
description: Optional[str] = None
variables: Optional[Dict[str, ServerVariable]] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class Reference(BaseModel):
attribute: Optional[bool] = None
wrapped: Optional[bool] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class ExternalDocumentation(BaseModel):
description: Optional[str] = None
url: AnyUrl
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class Schema(BaseModel):
),
] = None
- class Config:
- extra: str = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
# Ref: https://json-schema.org/draft/2020-12/json-schema-core.html#name-json-schema-documents
value: Optional[Any] = None
externalValue: Optional[AnyUrl] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class ParameterInType(Enum):
explode: Optional[bool] = None
allowReserved: Optional[bool] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class MediaType(BaseModel):
examples: Optional[Dict[str, Union[Example, Reference]]] = None
encoding: Optional[Dict[str, Encoding]] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class ParameterBase(BaseModel):
# Serialization rules for more complex scenarios
content: Optional[Dict[str, MediaType]] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class Parameter(ParameterBase):
content: Dict[str, MediaType]
required: Optional[bool] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class Link(BaseModel):
description: Optional[str] = None
server: Optional[Server] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class Response(BaseModel):
content: Optional[Dict[str, MediaType]] = None
links: Optional[Dict[str, Union[Link, Reference]]] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class Operation(BaseModel):
security: Optional[List[Dict[str, List[str]]]] = None
servers: Optional[List[Server]] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class PathItem(BaseModel):
servers: Optional[List[Server]] = None
parameters: Optional[List[Union[Parameter, Reference]]] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class SecuritySchemeType(Enum):
type_: SecuritySchemeType = Field(alias="type")
description: Optional[str] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class APIKeyIn(Enum):
refreshUrl: Optional[str] = None
scopes: Dict[str, str] = {}
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class OAuthFlowImplicit(OAuthFlow):
clientCredentials: Optional[OAuthFlowClientCredentials] = None
authorizationCode: Optional[OAuthFlowAuthorizationCode] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class OAuth2(SecurityBase):
callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None
pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class Tag(BaseModel):
description: Optional[str] = None
externalDocs: Optional[ExternalDocumentation] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
class OpenAPI(BaseModel):
tags: Optional[List[Tag]] = None
externalDocs: Optional[ExternalDocumentation] = None
- class Config:
- extra = "allow"
+ if PYDANTIC_V2:
+ model_config = {"extra": "allow"}
+
+ else:
+
+ class Config:
+ extra = "allow"
-Schema.update_forward_refs()
-Operation.update_forward_refs()
-Encoding.update_forward_refs()
+_model_rebuild(Schema)
+_model_rebuild(Operation)
+_model_rebuild(Encoding)
import http.client
import inspect
import warnings
-from enum import Enum
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast
from fastapi import routing
+from fastapi._compat import (
+ GenerateJsonSchema,
+ JsonSchemaValue,
+ ModelField,
+ Undefined,
+ get_compat_model_name_map,
+ get_definitions,
+ get_schema_from_model_field,
+ lenient_issubclass,
+)
from fastapi.datastructures import DefaultPlaceholder
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import get_flat_dependant, get_flat_params
from fastapi.encoders import jsonable_encoder
-from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX
+from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE
from fastapi.openapi.models import OpenAPI
from fastapi.params import Body, Param
from fastapi.responses import Response
+from fastapi.types import ModelNameMap
from fastapi.utils import (
deep_dict_update,
generate_operation_id_for_path,
- get_model_definitions,
is_body_allowed_for_status_code,
)
-from pydantic import BaseModel
-from pydantic.fields import ModelField, Undefined
-from pydantic.schema import (
- field_schema,
- get_flat_models_from_fields,
- get_model_name_map,
-)
-from pydantic.utils import lenient_issubclass
from starlette.responses import JSONResponse
from starlette.routing import BaseRoute
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
+from typing_extensions import Literal
validation_error_definition = {
"title": "ValidationError",
def get_openapi_operation_parameters(
*,
all_route_params: Sequence[ModelField],
- model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
+ schema_generator: GenerateJsonSchema,
+ model_name_map: ModelNameMap,
+ field_mapping: Dict[
+ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+ ],
) -> List[Dict[str, Any]]:
parameters = []
for param in all_route_params:
field_info = cast(Param, field_info)
if not field_info.include_in_schema:
continue
+ param_schema = get_schema_from_model_field(
+ field=param,
+ schema_generator=schema_generator,
+ model_name_map=model_name_map,
+ field_mapping=field_mapping,
+ )
parameter = {
"name": param.alias,
"in": field_info.in_.value,
"required": param.required,
- "schema": field_schema(
- param, model_name_map=model_name_map, ref_prefix=REF_PREFIX
- )[0],
+ "schema": param_schema,
}
if field_info.description:
parameter["description"] = field_info.description
def get_openapi_operation_request_body(
*,
body_field: Optional[ModelField],
- model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
+ schema_generator: GenerateJsonSchema,
+ model_name_map: ModelNameMap,
+ field_mapping: Dict[
+ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+ ],
) -> Optional[Dict[str, Any]]:
if not body_field:
return None
assert isinstance(body_field, ModelField)
- body_schema, _, _ = field_schema(
- body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
+ body_schema = get_schema_from_model_field(
+ field=body_field,
+ schema_generator=schema_generator,
+ model_name_map=model_name_map,
+ field_mapping=field_mapping,
)
field_info = cast(Body, body_field.field_info)
request_media_type = field_info.media_type
def get_openapi_path(
- *, route: routing.APIRoute, model_name_map: Dict[type, str], operation_ids: Set[str]
+ *,
+ route: routing.APIRoute,
+ operation_ids: Set[str],
+ schema_generator: GenerateJsonSchema,
+ model_name_map: ModelNameMap,
+ field_mapping: Dict[
+ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+ ],
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
path = {}
security_schemes: Dict[str, Any] = {}
security_schemes.update(security_definitions)
all_route_params = get_flat_params(route.dependant)
operation_parameters = get_openapi_operation_parameters(
- all_route_params=all_route_params, model_name_map=model_name_map
+ all_route_params=all_route_params,
+ schema_generator=schema_generator,
+ model_name_map=model_name_map,
+ field_mapping=field_mapping,
)
parameters.extend(operation_parameters)
if parameters:
operation["parameters"] = list(all_parameters.values())
if method in METHODS_WITH_BODY:
request_body_oai = get_openapi_operation_request_body(
- body_field=route.body_field, model_name_map=model_name_map
+ body_field=route.body_field,
+ schema_generator=schema_generator,
+ model_name_map=model_name_map,
+ field_mapping=field_mapping,
)
if request_body_oai:
operation["requestBody"] = request_body_oai
cb_definitions,
) = get_openapi_path(
route=callback,
- model_name_map=model_name_map,
operation_ids=operation_ids,
+ schema_generator=schema_generator,
+ model_name_map=model_name_map,
+ field_mapping=field_mapping,
)
callbacks[callback.name] = {callback.path: cb_path}
operation["callbacks"] = callbacks
response_schema = {"type": "string"}
if lenient_issubclass(current_response_class, JSONResponse):
if route.response_field:
- response_schema, _, _ = field_schema(
- route.response_field,
+ response_schema = get_schema_from_model_field(
+ field=route.response_field,
+ schema_generator=schema_generator,
model_name_map=model_name_map,
- ref_prefix=REF_PREFIX,
+ field_mapping=field_mapping,
)
else:
response_schema = {}
field = route.response_fields.get(additional_status_code)
additional_field_schema: Optional[Dict[str, Any]] = None
if field:
- additional_field_schema, _, _ = field_schema(
- field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
+ additional_field_schema = get_schema_from_model_field(
+ field=field,
+ schema_generator=schema_generator,
+ model_name_map=model_name_map,
+ field_mapping=field_mapping,
)
media_type = route_response_media_type or "application/json"
additional_schema = (
return path, security_schemes, definitions
-def get_flat_models_from_routes(
+def get_fields_from_routes(
routes: Sequence[BaseRoute],
-) -> Set[Union[Type[BaseModel], Type[Enum]]]:
+) -> List[ModelField]:
body_fields_from_routes: List[ModelField] = []
responses_from_routes: List[ModelField] = []
request_fields_from_routes: List[ModelField] = []
- callback_flat_models: Set[Union[Type[BaseModel], Type[Enum]]] = set()
+ callback_flat_models: List[ModelField] = []
for route in routes:
if getattr(route, "include_in_schema", None) and isinstance(
route, routing.APIRoute
if route.response_fields:
responses_from_routes.extend(route.response_fields.values())
if route.callbacks:
- callback_flat_models |= get_flat_models_from_routes(route.callbacks)
+ callback_flat_models.extend(get_fields_from_routes(route.callbacks))
params = get_flat_params(route.dependant)
request_fields_from_routes.extend(params)
- flat_models = callback_flat_models | get_flat_models_from_fields(
- body_fields_from_routes + responses_from_routes + request_fields_from_routes,
- known_models=set(),
+ flat_models = callback_flat_models + list(
+ body_fields_from_routes + responses_from_routes + request_fields_from_routes
)
return flat_models
paths: Dict[str, Dict[str, Any]] = {}
webhook_paths: Dict[str, Dict[str, Any]] = {}
operation_ids: Set[str] = set()
- flat_models = get_flat_models_from_routes(list(routes or []) + list(webhooks or []))
- model_name_map = get_model_name_map(flat_models)
- definitions = get_model_definitions(
- flat_models=flat_models, model_name_map=model_name_map
+ all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or []))
+ model_name_map = get_compat_model_name_map(all_fields)
+ schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
+ field_mapping, definitions = get_definitions(
+ fields=all_fields,
+ schema_generator=schema_generator,
+ model_name_map=model_name_map,
)
for route in routes or []:
if isinstance(route, routing.APIRoute):
result = get_openapi_path(
- route=route, model_name_map=model_name_map, operation_ids=operation_ids
+ route=route,
+ operation_ids=operation_ids,
+ schema_generator=schema_generator,
+ model_name_map=model_name_map,
+ field_mapping=field_mapping,
)
if result:
path, security_schemes, path_definitions = result
if isinstance(webhook, routing.APIRoute):
result = get_openapi_path(
route=webhook,
- model_name_map=model_name_map,
operation_ids=operation_ids,
+ schema_generator=schema_generator,
+ model_name_map=model_name_map,
+ field_mapping=field_mapping,
)
if result:
path, security_schemes, path_definitions = result
-from typing import Any, Callable, List, Optional, Sequence
+from typing import Any, Callable, Dict, List, Optional, Sequence, Union
from fastapi import params
-from pydantic.fields import Undefined
+from fastapi._compat import Undefined
from typing_extensions import Annotated, deprecated
+_Unset: Any = Undefined
+
def Path( # noqa: N802
default: Any = ...,
*,
+ default_factory: Union[Callable[[], Any], None] = _Unset,
alias: Optional[str] = None,
+ alias_priority: Union[int, None] = _Unset,
+ # TODO: update when deprecating Pydantic v1, import these types
+ # validation_alias: str | AliasPath | AliasChoices | None
+ validation_alias: Union[str, None] = None,
+ serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
- regex: Optional[str] = None,
+ pattern: Optional[str] = None,
+ regex: Annotated[
+ Optional[str],
+ deprecated(
+ "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+ ),
+ ] = None,
+ discriminator: Union[str, None] = None,
+ strict: Union[bool, None] = _Unset,
+ multiple_of: Union[float, None] = _Unset,
+ allow_inf_nan: Union[bool, None] = _Unset,
+ max_digits: Union[int, None] = _Unset,
+ decimal_places: Union[int, None] = _Unset,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
- ] = Undefined,
+ ] = _Unset,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
+ json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
) -> Any:
return params.Path(
default=default,
+ default_factory=default_factory,
alias=alias,
+ alias_priority=alias_priority,
+ validation_alias=validation_alias,
+ serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
le=le,
min_length=min_length,
max_length=max_length,
+ pattern=pattern,
regex=regex,
+ discriminator=discriminator,
+ strict=strict,
+ multiple_of=multiple_of,
+ allow_inf_nan=allow_inf_nan,
+ max_digits=max_digits,
+ decimal_places=decimal_places,
example=example,
examples=examples,
deprecated=deprecated,
include_in_schema=include_in_schema,
+ json_schema_extra=json_schema_extra,
**extra,
)
def Query( # noqa: N802
default: Any = Undefined,
*,
+ default_factory: Union[Callable[[], Any], None] = _Unset,
alias: Optional[str] = None,
+ alias_priority: Union[int, None] = _Unset,
+ # TODO: update when deprecating Pydantic v1, import these types
+ # validation_alias: str | AliasPath | AliasChoices | None
+ validation_alias: Union[str, None] = None,
+ serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
- regex: Optional[str] = None,
+ pattern: Optional[str] = None,
+ regex: Annotated[
+ Optional[str],
+ deprecated(
+ "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+ ),
+ ] = None,
+ discriminator: Union[str, None] = None,
+ strict: Union[bool, None] = _Unset,
+ multiple_of: Union[float, None] = _Unset,
+ allow_inf_nan: Union[bool, None] = _Unset,
+ max_digits: Union[int, None] = _Unset,
+ decimal_places: Union[int, None] = _Unset,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
- ] = Undefined,
+ ] = _Unset,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
+ json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
) -> Any:
return params.Query(
default=default,
+ default_factory=default_factory,
alias=alias,
+ alias_priority=alias_priority,
+ validation_alias=validation_alias,
+ serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
le=le,
min_length=min_length,
max_length=max_length,
+ pattern=pattern,
regex=regex,
+ discriminator=discriminator,
+ strict=strict,
+ multiple_of=multiple_of,
+ allow_inf_nan=allow_inf_nan,
+ max_digits=max_digits,
+ decimal_places=decimal_places,
example=example,
examples=examples,
deprecated=deprecated,
include_in_schema=include_in_schema,
+ json_schema_extra=json_schema_extra,
**extra,
)
def Header( # noqa: N802
default: Any = Undefined,
*,
+ default_factory: Union[Callable[[], Any], None] = _Unset,
alias: Optional[str] = None,
+ alias_priority: Union[int, None] = _Unset,
+ # TODO: update when deprecating Pydantic v1, import these types
+ # validation_alias: str | AliasPath | AliasChoices | None
+ validation_alias: Union[str, None] = None,
+ serialization_alias: Union[str, None] = None,
convert_underscores: bool = True,
title: Optional[str] = None,
description: Optional[str] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
- regex: Optional[str] = None,
+ pattern: Optional[str] = None,
+ regex: Annotated[
+ Optional[str],
+ deprecated(
+ "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+ ),
+ ] = None,
+ discriminator: Union[str, None] = None,
+ strict: Union[bool, None] = _Unset,
+ multiple_of: Union[float, None] = _Unset,
+ allow_inf_nan: Union[bool, None] = _Unset,
+ max_digits: Union[int, None] = _Unset,
+ decimal_places: Union[int, None] = _Unset,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
- ] = Undefined,
+ ] = _Unset,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
+ json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
) -> Any:
return params.Header(
default=default,
+ default_factory=default_factory,
alias=alias,
+ alias_priority=alias_priority,
+ validation_alias=validation_alias,
+ serialization_alias=serialization_alias,
convert_underscores=convert_underscores,
title=title,
description=description,
le=le,
min_length=min_length,
max_length=max_length,
+ pattern=pattern,
regex=regex,
+ discriminator=discriminator,
+ strict=strict,
+ multiple_of=multiple_of,
+ allow_inf_nan=allow_inf_nan,
+ max_digits=max_digits,
+ decimal_places=decimal_places,
example=example,
examples=examples,
deprecated=deprecated,
include_in_schema=include_in_schema,
+ json_schema_extra=json_schema_extra,
**extra,
)
def Cookie( # noqa: N802
default: Any = Undefined,
*,
+ default_factory: Union[Callable[[], Any], None] = _Unset,
alias: Optional[str] = None,
+ alias_priority: Union[int, None] = _Unset,
+ # TODO: update when deprecating Pydantic v1, import these types
+ # validation_alias: str | AliasPath | AliasChoices | None
+ validation_alias: Union[str, None] = None,
+ serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
- regex: Optional[str] = None,
+ pattern: Optional[str] = None,
+ regex: Annotated[
+ Optional[str],
+ deprecated(
+ "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+ ),
+ ] = None,
+ discriminator: Union[str, None] = None,
+ strict: Union[bool, None] = _Unset,
+ multiple_of: Union[float, None] = _Unset,
+ allow_inf_nan: Union[bool, None] = _Unset,
+ max_digits: Union[int, None] = _Unset,
+ decimal_places: Union[int, None] = _Unset,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
- ] = Undefined,
+ ] = _Unset,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
+ json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
) -> Any:
return params.Cookie(
default=default,
+ default_factory=default_factory,
alias=alias,
+ alias_priority=alias_priority,
+ validation_alias=validation_alias,
+ serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
le=le,
min_length=min_length,
max_length=max_length,
+ pattern=pattern,
regex=regex,
+ discriminator=discriminator,
+ strict=strict,
+ multiple_of=multiple_of,
+ allow_inf_nan=allow_inf_nan,
+ max_digits=max_digits,
+ decimal_places=decimal_places,
example=example,
examples=examples,
deprecated=deprecated,
include_in_schema=include_in_schema,
+ json_schema_extra=json_schema_extra,
**extra,
)
def Body( # noqa: N802
default: Any = Undefined,
*,
+ default_factory: Union[Callable[[], Any], None] = _Unset,
embed: bool = False,
media_type: str = "application/json",
alias: Optional[str] = None,
+ alias_priority: Union[int, None] = _Unset,
+ # TODO: update when deprecating Pydantic v1, import these types
+ # validation_alias: str | AliasPath | AliasChoices | None
+ validation_alias: Union[str, None] = None,
+ serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
- regex: Optional[str] = None,
+ pattern: Optional[str] = None,
+ regex: Annotated[
+ Optional[str],
+ deprecated(
+ "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+ ),
+ ] = None,
+ discriminator: Union[str, None] = None,
+ strict: Union[bool, None] = _Unset,
+ multiple_of: Union[float, None] = _Unset,
+ allow_inf_nan: Union[bool, None] = _Unset,
+ max_digits: Union[int, None] = _Unset,
+ decimal_places: Union[int, None] = _Unset,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
- ] = Undefined,
+ ] = _Unset,
+ deprecated: Optional[bool] = None,
+ include_in_schema: bool = True,
+ json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
) -> Any:
return params.Body(
default=default,
+ default_factory=default_factory,
embed=embed,
media_type=media_type,
alias=alias,
+ alias_priority=alias_priority,
+ validation_alias=validation_alias,
+ serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
le=le,
min_length=min_length,
max_length=max_length,
+ pattern=pattern,
regex=regex,
+ discriminator=discriminator,
+ strict=strict,
+ multiple_of=multiple_of,
+ allow_inf_nan=allow_inf_nan,
+ max_digits=max_digits,
+ decimal_places=decimal_places,
example=example,
examples=examples,
+ deprecated=deprecated,
+ include_in_schema=include_in_schema,
+ json_schema_extra=json_schema_extra,
**extra,
)
def Form( # noqa: N802
default: Any = Undefined,
*,
+ default_factory: Union[Callable[[], Any], None] = _Unset,
media_type: str = "application/x-www-form-urlencoded",
alias: Optional[str] = None,
+ alias_priority: Union[int, None] = _Unset,
+ # TODO: update when deprecating Pydantic v1, import these types
+ # validation_alias: str | AliasPath | AliasChoices | None
+ validation_alias: Union[str, None] = None,
+ serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
- regex: Optional[str] = None,
+ pattern: Optional[str] = None,
+ regex: Annotated[
+ Optional[str],
+ deprecated(
+ "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+ ),
+ ] = None,
+ discriminator: Union[str, None] = None,
+ strict: Union[bool, None] = _Unset,
+ multiple_of: Union[float, None] = _Unset,
+ allow_inf_nan: Union[bool, None] = _Unset,
+ max_digits: Union[int, None] = _Unset,
+ decimal_places: Union[int, None] = _Unset,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
- ] = Undefined,
+ ] = _Unset,
+ deprecated: Optional[bool] = None,
+ include_in_schema: bool = True,
+ json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
) -> Any:
return params.Form(
default=default,
+ default_factory=default_factory,
media_type=media_type,
alias=alias,
+ alias_priority=alias_priority,
+ validation_alias=validation_alias,
+ serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
le=le,
min_length=min_length,
max_length=max_length,
+ pattern=pattern,
regex=regex,
+ discriminator=discriminator,
+ strict=strict,
+ multiple_of=multiple_of,
+ allow_inf_nan=allow_inf_nan,
+ max_digits=max_digits,
+ decimal_places=decimal_places,
example=example,
examples=examples,
+ deprecated=deprecated,
+ include_in_schema=include_in_schema,
+ json_schema_extra=json_schema_extra,
**extra,
)
def File( # noqa: N802
default: Any = Undefined,
*,
+ default_factory: Union[Callable[[], Any], None] = _Unset,
media_type: str = "multipart/form-data",
alias: Optional[str] = None,
+ alias_priority: Union[int, None] = _Unset,
+ # TODO: update when deprecating Pydantic v1, import these types
+ # validation_alias: str | AliasPath | AliasChoices | None
+ validation_alias: Union[str, None] = None,
+ serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
- regex: Optional[str] = None,
+ pattern: Optional[str] = None,
+ regex: Annotated[
+ Optional[str],
+ deprecated(
+ "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+ ),
+ ] = None,
+ discriminator: Union[str, None] = None,
+ strict: Union[bool, None] = _Unset,
+ multiple_of: Union[float, None] = _Unset,
+ allow_inf_nan: Union[bool, None] = _Unset,
+ max_digits: Union[int, None] = _Unset,
+ decimal_places: Union[int, None] = _Unset,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
- ] = Undefined,
+ ] = _Unset,
+ deprecated: Optional[bool] = None,
+ include_in_schema: bool = True,
+ json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
) -> Any:
return params.File(
default=default,
+ default_factory=default_factory,
media_type=media_type,
alias=alias,
+ alias_priority=alias_priority,
+ validation_alias=validation_alias,
+ serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
le=le,
min_length=min_length,
max_length=max_length,
+ pattern=pattern,
regex=regex,
+ discriminator=discriminator,
+ strict=strict,
+ multiple_of=multiple_of,
+ allow_inf_nan=allow_inf_nan,
+ max_digits=max_digits,
+ decimal_places=decimal_places,
example=example,
examples=examples,
+ deprecated=deprecated,
+ include_in_schema=include_in_schema,
+ json_schema_extra=json_schema_extra,
**extra,
)
import warnings
from enum import Enum
-from typing import Any, Callable, List, Optional, Sequence
+from typing import Any, Callable, Dict, List, Optional, Sequence, Union
-from pydantic.fields import FieldInfo, Undefined
+from pydantic.fields import FieldInfo
from typing_extensions import Annotated, deprecated
+from ._compat import PYDANTIC_V2, Undefined
+
+_Unset: Any = Undefined
+
class ParamTypes(Enum):
query = "query"
self,
default: Any = Undefined,
*,
+ default_factory: Union[Callable[[], Any], None] = _Unset,
+ annotation: Optional[Any] = None,
alias: Optional[str] = None,
+ alias_priority: Union[int, None] = _Unset,
+ # TODO: update when deprecating Pydantic v1, import these types
+ # validation_alias: str | AliasPath | AliasChoices | None
+ validation_alias: Union[str, None] = None,
+ serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
- regex: Optional[str] = None,
+ pattern: Optional[str] = None,
+ regex: Annotated[
+ Optional[str],
+ deprecated(
+ "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+ ),
+ ] = None,
+ discriminator: Union[str, None] = None,
+ strict: Union[bool, None] = _Unset,
+ multiple_of: Union[float, None] = _Unset,
+ allow_inf_nan: Union[bool, None] = _Unset,
+ max_digits: Union[int, None] = _Unset,
+ decimal_places: Union[int, None] = _Unset,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
- ] = Undefined,
+ ] = _Unset,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
+ json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
):
self.deprecated = deprecated
- if example is not Undefined:
+ if example is not _Unset:
warnings.warn(
"`example` has been depreacated, please use `examples` instead",
category=DeprecationWarning,
- stacklevel=1,
+ stacklevel=4,
)
self.example = example
self.include_in_schema = include_in_schema
- extra_kwargs = {**extra}
- if examples:
- extra_kwargs["examples"] = examples
- super().__init__(
+ kwargs = dict(
default=default,
+ default_factory=default_factory,
alias=alias,
title=title,
description=description,
le=le,
min_length=min_length,
max_length=max_length,
- regex=regex,
- **extra_kwargs,
+ discriminator=discriminator,
+ multiple_of=multiple_of,
+ allow_nan=allow_inf_nan,
+ max_digits=max_digits,
+ decimal_places=decimal_places,
+ **extra,
)
+ if examples is not None:
+ kwargs["examples"] = examples
+ if regex is not None:
+ warnings.warn(
+ "`regex` has been depreacated, please use `pattern` instead",
+ category=DeprecationWarning,
+ stacklevel=4,
+ )
+ current_json_schema_extra = json_schema_extra or extra
+ if PYDANTIC_V2:
+ kwargs.update(
+ {
+ "annotation": annotation,
+ "alias_priority": alias_priority,
+ "validation_alias": validation_alias,
+ "serialization_alias": serialization_alias,
+ "strict": strict,
+ "json_schema_extra": current_json_schema_extra,
+ }
+ )
+ kwargs["pattern"] = pattern or regex
+ else:
+ kwargs["regex"] = pattern or regex
+ kwargs.update(**current_json_schema_extra)
+ use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
+
+ super().__init__(**use_kwargs)
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.default})"
self,
default: Any = ...,
*,
+ default_factory: Union[Callable[[], Any], None] = _Unset,
+ annotation: Optional[Any] = None,
alias: Optional[str] = None,
+ alias_priority: Union[int, None] = _Unset,
+ # TODO: update when deprecating Pydantic v1, import these types
+ # validation_alias: str | AliasPath | AliasChoices | None
+ validation_alias: Union[str, None] = None,
+ serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
- regex: Optional[str] = None,
+ pattern: Optional[str] = None,
+ regex: Annotated[
+ Optional[str],
+ deprecated(
+ "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+ ),
+ ] = None,
+ discriminator: Union[str, None] = None,
+ strict: Union[bool, None] = _Unset,
+ multiple_of: Union[float, None] = _Unset,
+ allow_inf_nan: Union[bool, None] = _Unset,
+ max_digits: Union[int, None] = _Unset,
+ decimal_places: Union[int, None] = _Unset,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
- ] = Undefined,
+ ] = _Unset,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
+ json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
):
assert default is ..., "Path parameters cannot have a default value"
self.in_ = self.in_
super().__init__(
default=default,
+ default_factory=default_factory,
+ annotation=annotation,
alias=alias,
+ alias_priority=alias_priority,
+ validation_alias=validation_alias,
+ serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
le=le,
min_length=min_length,
max_length=max_length,
+ pattern=pattern,
regex=regex,
+ discriminator=discriminator,
+ strict=strict,
+ multiple_of=multiple_of,
+ allow_inf_nan=allow_inf_nan,
+ max_digits=max_digits,
+ decimal_places=decimal_places,
deprecated=deprecated,
example=example,
examples=examples,
include_in_schema=include_in_schema,
+ json_schema_extra=json_schema_extra,
**extra,
)
self,
default: Any = Undefined,
*,
+ default_factory: Union[Callable[[], Any], None] = _Unset,
+ annotation: Optional[Any] = None,
alias: Optional[str] = None,
+ alias_priority: Union[int, None] = _Unset,
+ # TODO: update when deprecating Pydantic v1, import these types
+ # validation_alias: str | AliasPath | AliasChoices | None
+ validation_alias: Union[str, None] = None,
+ serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
- regex: Optional[str] = None,
+ pattern: Optional[str] = None,
+ regex: Annotated[
+ Optional[str],
+ deprecated(
+ "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+ ),
+ ] = None,
+ discriminator: Union[str, None] = None,
+ strict: Union[bool, None] = _Unset,
+ multiple_of: Union[float, None] = _Unset,
+ allow_inf_nan: Union[bool, None] = _Unset,
+ max_digits: Union[int, None] = _Unset,
+ decimal_places: Union[int, None] = _Unset,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
- ] = Undefined,
+ ] = _Unset,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
+ json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
):
super().__init__(
default=default,
+ default_factory=default_factory,
+ annotation=annotation,
alias=alias,
+ alias_priority=alias_priority,
+ validation_alias=validation_alias,
+ serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
le=le,
min_length=min_length,
max_length=max_length,
+ pattern=pattern,
regex=regex,
+ discriminator=discriminator,
+ strict=strict,
+ multiple_of=multiple_of,
+ allow_inf_nan=allow_inf_nan,
+ max_digits=max_digits,
+ decimal_places=decimal_places,
deprecated=deprecated,
example=example,
examples=examples,
include_in_schema=include_in_schema,
+ json_schema_extra=json_schema_extra,
**extra,
)
self,
default: Any = Undefined,
*,
+ default_factory: Union[Callable[[], Any], None] = _Unset,
+ annotation: Optional[Any] = None,
alias: Optional[str] = None,
+ alias_priority: Union[int, None] = _Unset,
+ # TODO: update when deprecating Pydantic v1, import these types
+ # validation_alias: str | AliasPath | AliasChoices | None
+ validation_alias: Union[str, None] = None,
+ serialization_alias: Union[str, None] = None,
convert_underscores: bool = True,
title: Optional[str] = None,
description: Optional[str] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
- regex: Optional[str] = None,
+ pattern: Optional[str] = None,
+ regex: Annotated[
+ Optional[str],
+ deprecated(
+ "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+ ),
+ ] = None,
+ discriminator: Union[str, None] = None,
+ strict: Union[bool, None] = _Unset,
+ multiple_of: Union[float, None] = _Unset,
+ allow_inf_nan: Union[bool, None] = _Unset,
+ max_digits: Union[int, None] = _Unset,
+ decimal_places: Union[int, None] = _Unset,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
- ] = Undefined,
+ ] = _Unset,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
+ json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
):
self.convert_underscores = convert_underscores
super().__init__(
default=default,
+ default_factory=default_factory,
+ annotation=annotation,
alias=alias,
+ alias_priority=alias_priority,
+ validation_alias=validation_alias,
+ serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
le=le,
min_length=min_length,
max_length=max_length,
+ pattern=pattern,
regex=regex,
+ discriminator=discriminator,
+ strict=strict,
+ multiple_of=multiple_of,
+ allow_inf_nan=allow_inf_nan,
+ max_digits=max_digits,
+ decimal_places=decimal_places,
deprecated=deprecated,
example=example,
examples=examples,
include_in_schema=include_in_schema,
+ json_schema_extra=json_schema_extra,
**extra,
)
self,
default: Any = Undefined,
*,
+ default_factory: Union[Callable[[], Any], None] = _Unset,
+ annotation: Optional[Any] = None,
alias: Optional[str] = None,
+ alias_priority: Union[int, None] = _Unset,
+ # TODO: update when deprecating Pydantic v1, import these types
+ # validation_alias: str | AliasPath | AliasChoices | None
+ validation_alias: Union[str, None] = None,
+ serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
- regex: Optional[str] = None,
+ pattern: Optional[str] = None,
+ regex: Annotated[
+ Optional[str],
+ deprecated(
+ "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+ ),
+ ] = None,
+ discriminator: Union[str, None] = None,
+ strict: Union[bool, None] = _Unset,
+ multiple_of: Union[float, None] = _Unset,
+ allow_inf_nan: Union[bool, None] = _Unset,
+ max_digits: Union[int, None] = _Unset,
+ decimal_places: Union[int, None] = _Unset,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
- ] = Undefined,
+ ] = _Unset,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
+ json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
):
super().__init__(
default=default,
+ default_factory=default_factory,
+ annotation=annotation,
alias=alias,
+ alias_priority=alias_priority,
+ validation_alias=validation_alias,
+ serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
le=le,
min_length=min_length,
max_length=max_length,
+ pattern=pattern,
regex=regex,
+ discriminator=discriminator,
+ strict=strict,
+ multiple_of=multiple_of,
+ allow_inf_nan=allow_inf_nan,
+ max_digits=max_digits,
+ decimal_places=decimal_places,
deprecated=deprecated,
example=example,
examples=examples,
include_in_schema=include_in_schema,
+ json_schema_extra=json_schema_extra,
**extra,
)
self,
default: Any = Undefined,
*,
+ default_factory: Union[Callable[[], Any], None] = _Unset,
+ annotation: Optional[Any] = None,
embed: bool = False,
media_type: str = "application/json",
alias: Optional[str] = None,
+ alias_priority: Union[int, None] = _Unset,
+ # TODO: update when deprecating Pydantic v1, import these types
+ # validation_alias: str | AliasPath | AliasChoices | None
+ validation_alias: Union[str, None] = None,
+ serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
- regex: Optional[str] = None,
+ pattern: Optional[str] = None,
+ regex: Annotated[
+ Optional[str],
+ deprecated(
+ "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+ ),
+ ] = None,
+ discriminator: Union[str, None] = None,
+ strict: Union[bool, None] = _Unset,
+ multiple_of: Union[float, None] = _Unset,
+ allow_inf_nan: Union[bool, None] = _Unset,
+ max_digits: Union[int, None] = _Unset,
+ decimal_places: Union[int, None] = _Unset,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
- ] = Undefined,
+ ] = _Unset,
+ deprecated: Optional[bool] = None,
+ include_in_schema: bool = True,
+ json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
):
self.embed = embed
self.media_type = media_type
- if example is not Undefined:
+ self.deprecated = deprecated
+ if example is not _Unset:
warnings.warn(
"`example` has been depreacated, please use `examples` instead",
category=DeprecationWarning,
- stacklevel=1,
+ stacklevel=4,
)
self.example = example
- extra_kwargs = {**extra}
- if examples is not None:
- extra_kwargs["examples"] = examples
- super().__init__(
+ self.include_in_schema = include_in_schema
+ kwargs = dict(
default=default,
+ default_factory=default_factory,
alias=alias,
title=title,
description=description,
le=le,
min_length=min_length,
max_length=max_length,
- regex=regex,
- **extra_kwargs,
+ discriminator=discriminator,
+ multiple_of=multiple_of,
+ allow_nan=allow_inf_nan,
+ max_digits=max_digits,
+ decimal_places=decimal_places,
+ **extra,
)
+ if examples is not None:
+ kwargs["examples"] = examples
+ if regex is not None:
+ warnings.warn(
+ "`regex` has been depreacated, please use `pattern` instead",
+ category=DeprecationWarning,
+ stacklevel=4,
+ )
+ current_json_schema_extra = json_schema_extra or extra
+ if PYDANTIC_V2:
+ kwargs.update(
+ {
+ "annotation": annotation,
+ "alias_priority": alias_priority,
+ "validation_alias": validation_alias,
+ "serialization_alias": serialization_alias,
+ "strict": strict,
+ "json_schema_extra": current_json_schema_extra,
+ }
+ )
+ kwargs["pattern"] = pattern or regex
+ else:
+ kwargs["regex"] = pattern or regex
+ kwargs.update(**current_json_schema_extra)
+
+ use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
+
+ super().__init__(**use_kwargs)
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.default})"
self,
default: Any = Undefined,
*,
+ default_factory: Union[Callable[[], Any], None] = _Unset,
+ annotation: Optional[Any] = None,
media_type: str = "application/x-www-form-urlencoded",
alias: Optional[str] = None,
+ alias_priority: Union[int, None] = _Unset,
+ # TODO: update when deprecating Pydantic v1, import these types
+ # validation_alias: str | AliasPath | AliasChoices | None
+ validation_alias: Union[str, None] = None,
+ serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
- regex: Optional[str] = None,
+ pattern: Optional[str] = None,
+ regex: Annotated[
+ Optional[str],
+ deprecated(
+ "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+ ),
+ ] = None,
+ discriminator: Union[str, None] = None,
+ strict: Union[bool, None] = _Unset,
+ multiple_of: Union[float, None] = _Unset,
+ allow_inf_nan: Union[bool, None] = _Unset,
+ max_digits: Union[int, None] = _Unset,
+ decimal_places: Union[int, None] = _Unset,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
- ] = Undefined,
+ ] = _Unset,
+ deprecated: Optional[bool] = None,
+ include_in_schema: bool = True,
+ json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
):
super().__init__(
default=default,
+ default_factory=default_factory,
+ annotation=annotation,
embed=True,
media_type=media_type,
alias=alias,
+ alias_priority=alias_priority,
+ validation_alias=validation_alias,
+ serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
le=le,
min_length=min_length,
max_length=max_length,
+ pattern=pattern,
regex=regex,
+ discriminator=discriminator,
+ strict=strict,
+ multiple_of=multiple_of,
+ allow_inf_nan=allow_inf_nan,
+ max_digits=max_digits,
+ decimal_places=decimal_places,
+ deprecated=deprecated,
example=example,
examples=examples,
+ include_in_schema=include_in_schema,
+ json_schema_extra=json_schema_extra,
**extra,
)
self,
default: Any = Undefined,
*,
+ default_factory: Union[Callable[[], Any], None] = _Unset,
+ annotation: Optional[Any] = None,
media_type: str = "multipart/form-data",
alias: Optional[str] = None,
+ alias_priority: Union[int, None] = _Unset,
+ # TODO: update when deprecating Pydantic v1, import these types
+ # validation_alias: str | AliasPath | AliasChoices | None
+ validation_alias: Union[str, None] = None,
+ serialization_alias: Union[str, None] = None,
title: Optional[str] = None,
description: Optional[str] = None,
gt: Optional[float] = None,
le: Optional[float] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
- regex: Optional[str] = None,
+ pattern: Optional[str] = None,
+ regex: Annotated[
+ Optional[str],
+ deprecated(
+ "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+ ),
+ ] = None,
+ discriminator: Union[str, None] = None,
+ strict: Union[bool, None] = _Unset,
+ multiple_of: Union[float, None] = _Unset,
+ allow_inf_nan: Union[bool, None] = _Unset,
+ max_digits: Union[int, None] = _Unset,
+ decimal_places: Union[int, None] = _Unset,
examples: Optional[List[Any]] = None,
example: Annotated[
Optional[Any],
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
"although still supported. Use examples instead."
),
- ] = Undefined,
+ ] = _Unset,
+ deprecated: Optional[bool] = None,
+ include_in_schema: bool = True,
+ json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
):
super().__init__(
default=default,
+ default_factory=default_factory,
+ annotation=annotation,
media_type=media_type,
alias=alias,
+ alias_priority=alias_priority,
+ validation_alias=validation_alias,
+ serialization_alias=serialization_alias,
title=title,
description=description,
gt=gt,
le=le,
min_length=min_length,
max_length=max_length,
+ pattern=pattern,
regex=regex,
+ discriminator=discriminator,
+ strict=strict,
+ multiple_of=multiple_of,
+ allow_inf_nan=allow_inf_nan,
+ max_digits=max_digits,
+ decimal_places=decimal_places,
+ deprecated=deprecated,
example=example,
examples=examples,
+ include_in_schema=include_in_schema,
+ json_schema_extra=json_schema_extra,
**extra,
)
)
from fastapi import params
+from fastapi._compat import (
+ ModelField,
+ Undefined,
+ _get_model_config,
+ _model_dump,
+ _normalize_errors,
+ lenient_issubclass,
+)
from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import (
get_typed_return_annotation,
solve_dependencies,
)
-from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
+from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import (
FastAPIError,
RequestValidationError,
+ ResponseValidationError,
WebSocketRequestValidationError,
)
-from fastapi.types import DecoratedCallable
+from fastapi.types import DecoratedCallable, IncEx
from fastapi.utils import (
create_cloned_field,
create_response_field,
is_body_allowed_for_status_code,
)
from pydantic import BaseModel
-from pydantic.error_wrappers import ErrorWrapper, ValidationError
-from pydantic.fields import ModelField, Undefined
-from pydantic.utils import lenient_issubclass
from starlette import routing
from starlette.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException
exclude_none: bool = False,
) -> Any:
if isinstance(res, BaseModel):
- read_with_orm_mode = getattr(res.__config__, "read_with_orm_mode", None)
+ read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None)
if read_with_orm_mode:
# Let from_orm extract the data from this model instead of converting
# it now to a dict.
# Otherwise there's no way to extract lazy data that requires attribute
# access instead of dict iteration, e.g. lazy relationships.
return res
- return res.dict(
+ return _model_dump(
+ res,
by_alias=True,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
*,
field: Optional[ModelField] = None,
response_content: Any,
- include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ include: Optional[IncEx] = None,
+ exclude: Optional[IncEx] = None,
by_alias: bool = True,
exclude_unset: bool = False,
exclude_defaults: bool = False,
) -> Any:
if field:
errors = []
- response_content = _prepare_response_content(
- response_content,
- exclude_unset=exclude_unset,
- exclude_defaults=exclude_defaults,
- exclude_none=exclude_none,
- )
+ if not hasattr(field, "serialize"):
+ # pydantic v1
+ response_content = _prepare_response_content(
+ response_content,
+ exclude_unset=exclude_unset,
+ exclude_defaults=exclude_defaults,
+ exclude_none=exclude_none,
+ )
if is_coroutine:
value, errors_ = field.validate(response_content, {}, loc=("response",))
else:
value, errors_ = await run_in_threadpool(
field.validate, response_content, {}, loc=("response",)
)
- if isinstance(errors_, ErrorWrapper):
- errors.append(errors_)
- elif isinstance(errors_, list):
+ if isinstance(errors_, list):
errors.extend(errors_)
+ elif errors_:
+ errors.append(errors_)
if errors:
- raise ValidationError(errors, field.type_)
+ raise ResponseValidationError(
+ errors=_normalize_errors(errors), body=response_content
+ )
+
+ if hasattr(field, "serialize"):
+ return field.serialize(
+ value,
+ include=include,
+ exclude=exclude,
+ by_alias=by_alias,
+ exclude_unset=exclude_unset,
+ exclude_defaults=exclude_defaults,
+ exclude_none=exclude_none,
+ )
+
return jsonable_encoder(
value,
include=include,
status_code: Optional[int] = None,
response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse),
response_field: Optional[ModelField] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
body = body_bytes
except json.JSONDecodeError as e:
raise RequestValidationError(
- [ErrorWrapper(e, ("body", e.pos))], body=e.doc
+ [
+ {
+ "type": "json_invalid",
+ "loc": ("body", e.pos),
+ "msg": "JSON decode error",
+ "input": {},
+ "ctx": {"error": e.msg},
+ }
+ ],
+ body=e.doc,
) from e
except HTTPException:
raise
)
values, errors, background_tasks, sub_response, _ = solved_result
if errors:
- raise RequestValidationError(errors, body=body)
+ raise RequestValidationError(_normalize_errors(errors), body=body)
else:
raw_response = await run_endpoint_function(
dependant=dependant, values=values, is_coroutine=is_coroutine
)
values, errors, _, _2, _3 = solved_result
if errors:
- raise WebSocketRequestValidationError(errors)
+ raise WebSocketRequestValidationError(_normalize_errors(errors))
assert dependant.call is not None, "dependant.call must be a function"
await dependant.call(**values)
name: Optional[str] = None,
methods: Optional[Union[Set[str], List[str]]] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
), f"Status code {status_code} must not have a response body"
response_name = "Response_" + self.unique_id
self.response_field = create_response_field(
- name=response_name, type_=self.response_model
+ name=response_name,
+ type_=self.response_model,
+ # TODO: This should actually set mode='serialization', just, that changes the schemas
+ # mode="serialization",
+ mode="validation",
)
# 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
# would pass the validation and be returned as is.
# By being a new field, no inheritance will be passed as is. A new model
# will be always created.
+ # TODO: remove when deprecating Pydantic v1
self.secure_cloned_response_field: Optional[
ModelField
] = create_cloned_field(self.response_field)
deprecated: Optional[bool] = None,
methods: Optional[Union[Set[str], List[str]]] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
- response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
- response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
+ response_model_include: Optional[IncEx] = None,
+ response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
from starlette.requests import Request
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
+# TODO: import from typing when deprecating Python 3.9
+from typing_extensions import Annotated
+
class OAuth2PasswordRequestForm:
"""
def __init__(
self,
- grant_type: str = Form(default=None, regex="password"),
- username: str = Form(),
- password: str = Form(),
- scope: str = Form(default=""),
- client_id: Optional[str] = Form(default=None),
- client_secret: Optional[str] = Form(default=None),
+ *,
+ grant_type: Annotated[Union[str, None], Form(pattern="password")] = None,
+ username: Annotated[str, Form()],
+ password: Annotated[str, Form()],
+ scope: Annotated[str, Form()] = "",
+ client_id: Annotated[Union[str, None], Form()] = None,
+ client_secret: Annotated[Union[str, None], Form()] = None,
):
self.grant_type = grant_type
self.username = username
def __init__(
self,
- grant_type: str = Form(regex="password"),
- username: str = Form(),
- password: str = Form(),
- scope: str = Form(default=""),
- client_id: Optional[str] = Form(default=None),
- client_secret: Optional[str] = Form(default=None),
+ grant_type: Annotated[str, Form(pattern="password")],
+ username: Annotated[str, Form()],
+ password: Annotated[str, Form()],
+ scope: Annotated[str, Form()] = "",
+ client_id: Annotated[Union[str, None], Form()] = None,
+ client_secret: Annotated[Union[str, None], Form()] = None,
):
super().__init__(
grant_type=grant_type,
-from typing import Any, Callable, TypeVar
+import types
+from enum import Enum
+from typing import Any, Callable, Dict, Set, Type, TypeVar, Union
+
+from pydantic import BaseModel
DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any])
+UnionType = getattr(types, "UnionType", Union)
+NoneType = getattr(types, "UnionType", None)
+ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str]
+IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]]
import re
import warnings
from dataclasses import is_dataclass
-from enum import Enum
from typing import (
TYPE_CHECKING,
Any,
from weakref import WeakKeyDictionary
import fastapi
+from fastapi._compat import (
+ PYDANTIC_V2,
+ BaseConfig,
+ ModelField,
+ PydanticSchemaGenerationError,
+ Undefined,
+ UndefinedType,
+ Validator,
+ lenient_issubclass,
+)
from fastapi.datastructures import DefaultPlaceholder, DefaultType
-from fastapi.openapi.constants import REF_PREFIX
-from pydantic import BaseConfig, BaseModel, create_model
-from pydantic.class_validators import Validator
-from pydantic.fields import FieldInfo, ModelField, UndefinedType
-from pydantic.schema import model_process_schema
-from pydantic.utils import lenient_issubclass
+from pydantic import BaseModel, create_model
+from pydantic.fields import FieldInfo
+from typing_extensions import Literal
if TYPE_CHECKING: # pragma: nocover
from .routing import APIRoute
return not (current_status_code < 200 or current_status_code in {204, 304})
-def get_model_definitions(
- *,
- flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
- model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
-) -> Dict[str, Any]:
- definitions: Dict[str, Dict[str, Any]] = {}
- for model in flat_models:
- m_schema, m_definitions, m_nested_models = model_process_schema(
- model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
- )
- definitions.update(m_definitions)
- model_name = model_name_map[model]
- if "description" in m_schema:
- m_schema["description"] = m_schema["description"].split("\f")[0]
- definitions[model_name] = m_schema
- return definitions
-
-
def get_path_param_names(path: str) -> Set[str]:
return set(re.findall("{(.*?)}", path))
name: str,
type_: Type[Any],
class_validators: Optional[Dict[str, Validator]] = None,
- default: Optional[Any] = None,
- required: Union[bool, UndefinedType] = True,
+ default: Optional[Any] = Undefined,
+ required: Union[bool, UndefinedType] = Undefined,
model_config: Type[BaseConfig] = BaseConfig,
field_info: Optional[FieldInfo] = None,
alias: Optional[str] = None,
+ mode: Literal["validation", "serialization"] = "validation",
) -> ModelField:
"""
Create a new response field. Raises if type_ is invalid.
"""
class_validators = class_validators or {}
- field_info = field_info or FieldInfo()
-
- try:
- return ModelField(
- name=name,
- type_=type_,
- class_validators=class_validators,
- default=default,
- required=required,
- model_config=model_config,
- alias=alias,
- field_info=field_info,
+ if PYDANTIC_V2:
+ field_info = field_info or FieldInfo(
+ annotation=type_, default=default, alias=alias
+ )
+ else:
+ field_info = field_info or FieldInfo()
+ kwargs = {"name": name, "field_info": field_info}
+ if PYDANTIC_V2:
+ kwargs.update({"mode": mode})
+ else:
+ kwargs.update(
+ {
+ "type_": type_,
+ "class_validators": class_validators,
+ "default": default,
+ "required": required,
+ "model_config": model_config,
+ "alias": alias,
+ }
)
- except RuntimeError:
+ try:
+ return ModelField(**kwargs) # type: ignore[arg-type]
+ except (RuntimeError, PydanticSchemaGenerationError):
raise fastapi.exceptions.FastAPIError(
"Invalid args for response field! Hint: "
f"check that {type_} is a valid Pydantic field type. "
*,
cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None,
) -> ModelField:
+ if PYDANTIC_V2:
+ return field
# cloned_types caches already cloned types to support recursive models and improve
# performance by avoiding unecessary cloning
if cloned_types is None:
f, cloned_types=cloned_types
)
new_field = create_response_field(name=field.name, type_=use_type)
- new_field.has_alias = field.has_alias
- new_field.alias = field.alias
- new_field.class_validators = field.class_validators
- new_field.default = field.default
- new_field.required = field.required
- new_field.model_config = field.model_config
+ new_field.has_alias = field.has_alias # type: ignore[attr-defined]
+ new_field.alias = field.alias # type: ignore[misc]
+ new_field.class_validators = field.class_validators # type: ignore[attr-defined]
+ new_field.default = field.default # type: ignore[misc]
+ new_field.required = field.required # type: ignore[misc]
+ new_field.model_config = field.model_config # type: ignore[attr-defined]
new_field.field_info = field.field_info
- new_field.allow_none = field.allow_none
- new_field.validate_always = field.validate_always
- if field.sub_fields:
- new_field.sub_fields = [
+ new_field.allow_none = field.allow_none # type: ignore[attr-defined]
+ new_field.validate_always = field.validate_always # type: ignore[attr-defined]
+ if field.sub_fields: # type: ignore[attr-defined]
+ new_field.sub_fields = [ # type: ignore[attr-defined]
create_cloned_field(sub_field, cloned_types=cloned_types)
- for sub_field in field.sub_fields
+ for sub_field in field.sub_fields # type: ignore[attr-defined]
]
- if field.key_field:
- new_field.key_field = create_cloned_field(
- field.key_field, cloned_types=cloned_types
+ if field.key_field: # type: ignore[attr-defined]
+ new_field.key_field = create_cloned_field( # type: ignore[attr-defined]
+ field.key_field, cloned_types=cloned_types # type: ignore[attr-defined]
)
- new_field.validators = field.validators
- new_field.pre_validators = field.pre_validators
- new_field.post_validators = field.post_validators
- new_field.parse_json = field.parse_json
- new_field.shape = field.shape
- new_field.populate_validators()
+ new_field.validators = field.validators # type: ignore[attr-defined]
+ new_field.pre_validators = field.pre_validators # type: ignore[attr-defined]
+ new_field.post_validators = field.post_validators # type: ignore[attr-defined]
+ new_field.parse_json = field.parse_json # type: ignore[attr-defined]
+ new_field.shape = field.shape # type: ignore[attr-defined]
+ new_field.populate_validators() # type: ignore[attr-defined]
return new_field
if not isinstance(item, DefaultPlaceholder):
return item
return first_item
+
+
+def match_pydantic_error_url(error_type: str) -> Any:
+ from dirty_equals import IsStr
+
+ return IsStr(regex=rf"^https://errors\.pydantic\.dev/.*/v/{error_type}")
]
dependencies = [
"starlette>=0.27.0,<0.28.0",
- "pydantic>=1.7.4,!=1.8,!=1.8.1,<2.0.0",
- "typing-extensions>=4.5.0"
+ "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,<3.0.0",
+ "typing-extensions>=4.5.0",
]
dynamic = ["version"]
"pyyaml >=5.3.1",
"ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0",
"orjson >=3.2.1",
- "email_validator >=1.1.1",
+ "email_validator >=2.0.0",
"uvicorn[standard] >=0.12.0",
+ "pydantic-settings >=2.0.0",
+ "pydantic-extra-types >=2.0.0",
]
[tool.hatch.version]
addopts = [
"--strict-config",
"--strict-markers",
+ "--ignore=docs_src",
]
xfail_strict = true
junit_family = "xunit2"
"docs_src/custom_response/tutorial007.py" = ["B007"]
"docs_src/dataclasses/tutorial003.py" = ["I001"]
"docs_src/path_operation_advanced_configuration/tutorial007.py" = ["B904"]
+"docs_src/path_operation_advanced_configuration/tutorial007_pv1.py" = ["B904"]
"docs_src/custom_request_and_route/tutorial002.py" = ["B904"]
"docs_src/dependencies/tutorial008_an.py" = ["F821"]
"docs_src/dependencies/tutorial008_an_py39.py" = ["F821"]
-e .
+pydantic-settings >=2.0.0
pytest >=7.1.3,<8.0.0
coverage[toml] >= 6.5.0,< 8.0
mypy ==1.4.0
ruff ==0.0.275
black == 23.3.0
httpx >=0.23.0,<0.25.0
-email_validator >=1.1.1,<2.0.0
+email_validator >=1.1.1,<3.0.0
+dirty-equals ==0.6.0
# TODO: once removing databases from tutorial, upgrade SQLAlchemy
# probably when including SQLModel
sqlalchemy >=1.3.18,<1.4.43
from typing import Union
+from dirty_equals import IsDict
from fastapi import FastAPI
+from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
-from pydantic import BaseModel
+from pydantic import BaseModel, ConfigDict
class FooBaseModel(BaseModel):
- class Config:
- extra = "forbid"
+ if PYDANTIC_V2:
+ model_config = ConfigDict(extra="forbid")
+ else:
+
+ class Config:
+ extra = "forbid"
class Foo(FooBaseModel):
"requestBody": {
"content": {
"application/json": {
- "schema": {"$ref": "#/components/schemas/Foo"}
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {"$ref": "#/components/schemas/Foo"},
+ {"type": "null"},
+ ],
+ "title": "Foo",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"$ref": "#/components/schemas/Foo"}
+ )
}
}
},
+from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, HttpUrl
"parameters": [
{
"required": True,
- "schema": {
- "title": "Callback Url",
- "maxLength": 2083,
- "minLength": 1,
- "type": "string",
- "format": "uri",
- },
+ "schema": IsDict(
+ {
+ "title": "Callback Url",
+ "minLength": 1,
+ "type": "string",
+ "format": "uri",
+ }
+ )
+ # TODO: remove when deprecating Pydantic v1
+ | IsDict(
+ {
+ "title": "Callback Url",
+ "maxLength": 2083,
+ "minLength": 1,
+ "type": "string",
+ "format": "uri",
+ }
+ ),
"name": "callback_url",
"in": "query",
}
import pytest
+from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI, Query
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from typing_extensions import Annotated
app = FastAPI()
foo_is_missing = {
"detail": [
- {
- "loc": ["query", "foo"],
- "msg": "field required",
- "type": "value_error.missing",
- }
+ IsDict(
+ {
+ "loc": ["query", "foo"],
+ "msg": "Field required",
+ "type": "missing",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ )
+ # TODO: remove when deprecating Pydantic v1
+ | IsDict(
+ {
+ "loc": ["query", "foo"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ )
]
}
foo_is_short = {
"detail": [
- {
- "ctx": {"limit_value": 1},
- "loc": ["query", "foo"],
- "msg": "ensure this value has at least 1 characters",
- "type": "value_error.any_str.min_length",
- }
+ IsDict(
+ {
+ "ctx": {"min_length": 1},
+ "loc": ["query", "foo"],
+ "msg": "String should have at least 1 characters",
+ "type": "string_too_short",
+ "input": "",
+ "url": match_pydantic_error_url("string_too_short"),
+ }
+ )
+ # TODO: remove when deprecating Pydantic v1
+ | IsDict(
+ {
+ "ctx": {"limit_value": 1},
+ "loc": ["query", "foo"],
+ "msg": "ensure this value has at least 1 characters",
+ "type": "value_error.any_str.min_length",
+ }
+ )
]
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from .main import app
"operationId": "get_path_param_id_path_param__item_id__get",
"parameters": [
{
- "required": True,
- "schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
+ "required": True,
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Item Id",
+ }
+ )
+ # TODO: remove when deprecating Pydantic v1
+ | IsDict({"title": "Item Id", "type": "string"}),
}
],
}
"operationId": "get_query_type_optional_query_int_optional_get",
"parameters": [
{
- "required": False,
- "schema": {"title": "Query", "type": "integer"},
"name": "query",
"in": "query",
+ "required": False,
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "integer"}, {"type": "null"}],
+ "title": "Query",
+ }
+ )
+ # TODO: remove when deprecating Pydantic v1
+ | IsDict({"title": "Query", "type": "integer"}),
}
],
}
--- /dev/null
+from typing import List, Union
+
+from fastapi import FastAPI, UploadFile
+from fastapi._compat import (
+ ModelField,
+ Undefined,
+ _get_model_config,
+ is_bytes_sequence_annotation,
+ is_uploadfile_sequence_annotation,
+)
+from fastapi.testclient import TestClient
+from pydantic import BaseConfig, BaseModel, ConfigDict
+from pydantic.fields import FieldInfo
+
+from .utils import needs_pydanticv1, needs_pydanticv2
+
+
+@needs_pydanticv2
+def test_model_field_default_required():
+ # For coverage
+ field_info = FieldInfo(annotation=str)
+ field = ModelField(name="foo", field_info=field_info)
+ assert field.default is Undefined
+
+
+@needs_pydanticv1
+def test_upload_file_dummy_general_plain_validator_function():
+ # For coverage
+ assert UploadFile.__get_pydantic_core_schema__(str, lambda x: None) == {}
+
+
+@needs_pydanticv1
+def test_union_scalar_list():
+ # For coverage
+ # TODO: there might not be a current valid code path that uses this, it would
+ # potentially enable query parameters defined as both a scalar and a list
+ # but that would require more refactors, also not sure it's really useful
+ from fastapi._compat import is_pv1_scalar_field
+
+ field_info = FieldInfo()
+ field = ModelField(
+ name="foo",
+ field_info=field_info,
+ type_=Union[str, List[int]],
+ class_validators={},
+ model_config=BaseConfig,
+ )
+ assert not is_pv1_scalar_field(field)
+
+
+@needs_pydanticv2
+def test_get_model_config():
+ # For coverage in Pydantic v2
+ class Foo(BaseModel):
+ model_config = ConfigDict(from_attributes=True)
+
+ foo = Foo()
+ config = _get_model_config(foo)
+ assert config == {"from_attributes": True}
+
+
+def test_complex():
+ app = FastAPI()
+
+ @app.post("/")
+ def foo(foo: Union[str, List[int]]):
+ return foo
+
+ client = TestClient(app)
+
+ response = client.post("/", json="bar")
+ assert response.status_code == 200, response.text
+ assert response.json() == "bar"
+
+ response2 = client.post("/", json=[1, 2])
+ assert response2.status_code == 200, response2.text
+ assert response2.json() == [1, 2]
+
+
+def test_is_bytes_sequence_annotation_union():
+ # For coverage
+ # TODO: in theory this would allow declaring types that could be lists of bytes
+ # to be read from files and other types, but I'm not even sure it's a good idea
+ # to support it as a first class "feature"
+ assert is_bytes_sequence_annotation(Union[List[str], List[bytes]])
+
+
+def test_is_uploadfile_sequence_annotation():
+ # For coverage
+ # TODO: in theory this would allow declaring types that could be lists of UploadFile
+ # and other types, but I'm not even sure it's a good idea to support it as a first
+ # class "feature"
+ assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]])
from fastapi import FastAPI
+from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel
class Item(BaseModel):
name: str
- class Config:
- schema_extra = {
- "x-something-internal": {"level": 4},
+ if PYDANTIC_V2:
+ model_config = {
+ "json_schema_extra": {
+ "x-something-internal": {"level": 4},
+ }
}
+ else:
+
+ class Config:
+ schema_extra = {
+ "x-something-internal": {"level": 4},
+ }
@app.get("/foo", response_model=Item)
from fastapi.testclient import TestClient
+# TODO: remove when deprecating Pydantic v1
def test_upload_file_invalid():
with pytest.raises(ValueError):
UploadFile.validate("not a Starlette UploadFile")
+def test_upload_file_invalid_pydantic_v2():
+ with pytest.raises(ValueError):
+ UploadFile._validate("not a Starlette UploadFile", {})
+
+
def test_default_placeholder_equals():
placeholder_1 = Default("a")
placeholder_2 = Default("a")
from fastapi.testclient import TestClient
from pydantic import BaseModel
+from .utils import needs_pydanticv1, needs_pydanticv2
-class ModelWithDatetimeField(BaseModel):
- dt_field: datetime
- class Config:
- json_encoders = {
- datetime: lambda dt: dt.replace(
- microsecond=0, tzinfo=timezone.utc
- ).isoformat()
- }
+@needs_pydanticv2
+def test_pydanticv2():
+ from pydantic import field_serializer
+ class ModelWithDatetimeField(BaseModel):
+ dt_field: datetime
-app = FastAPI()
-model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
+ @field_serializer("dt_field")
+ def serialize_datetime(self, dt_field: datetime):
+ return dt_field.replace(microsecond=0, tzinfo=timezone.utc).isoformat()
+ app = FastAPI()
+ model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
-@app.get("/model", response_model=ModelWithDatetimeField)
-def get_model():
- return model
+ @app.get("/model", response_model=ModelWithDatetimeField)
+ def get_model():
+ return model
+ client = TestClient(app)
+ with client:
+ response = client.get("/model")
+ assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+def test_pydanticv1():
+ class ModelWithDatetimeField(BaseModel):
+ dt_field: datetime
+
+ class Config:
+ json_encoders = {
+ datetime: lambda dt: dt.replace(
+ microsecond=0, tzinfo=timezone.utc
+ ).isoformat()
+ }
-client = TestClient(app)
+ app = FastAPI()
+ model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
+ @app.get("/model", response_model=ModelWithDatetimeField)
+ def get_model():
+ return model
-def test_dt():
+ client = TestClient(app)
with client:
response = client.get("/model")
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}
from typing import List
+from dirty_equals import IsDict
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel
app = FastAPI()
def test_no_duplicates_invalid():
response = client.post("/no-duplicates", json={"item": {"data": "myitem"}})
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["body", "item2"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "item2"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "item2"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
def test_no_duplicates():
from typing import Optional
import pytest
+from dirty_equals import IsDict
from fastapi import APIRouter, Depends, FastAPI
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
app = FastAPI()
return msg
-@pytest.mark.parametrize(
- "url,status_code,expected",
- [
- (
- "/main-depends/",
- 422,
- {
- "detail": [
- {
- "loc": ["query", "q"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- ),
- (
- "/main-depends/?q=foo",
- 200,
- {"in": "main-depends", "params": {"q": "foo", "skip": 0, "limit": 100}},
- ),
- (
- "/main-depends/?q=foo&skip=100&limit=200",
- 200,
- {"in": "main-depends", "params": {"q": "foo", "skip": 100, "limit": 200}},
- ),
- (
- "/decorator-depends/",
- 422,
- {
- "detail": [
- {
- "loc": ["query", "q"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- ),
- ("/decorator-depends/?q=foo", 200, {"in": "decorator-depends"}),
- (
- "/decorator-depends/?q=foo&skip=100&limit=200",
- 200,
- {"in": "decorator-depends"},
- ),
- (
- "/router-depends/",
- 422,
- {
- "detail": [
- {
- "loc": ["query", "q"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- ),
- (
- "/router-depends/?q=foo",
- 200,
- {"in": "router-depends", "params": {"q": "foo", "skip": 0, "limit": 100}},
- ),
- (
- "/router-depends/?q=foo&skip=100&limit=200",
- 200,
- {"in": "router-depends", "params": {"q": "foo", "skip": 100, "limit": 200}},
- ),
- (
- "/router-decorator-depends/",
- 422,
- {
- "detail": [
- {
- "loc": ["query", "q"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- ),
- ("/router-decorator-depends/?q=foo", 200, {"in": "router-decorator-depends"}),
- (
- "/router-decorator-depends/?q=foo&skip=100&limit=200",
- 200,
- {"in": "router-decorator-depends"},
- ),
- ],
-)
-def test_normal_app(url, status_code, expected):
- response = client.get(url)
- assert response.status_code == status_code
- assert response.json() == expected
+def test_main_depends():
+ response = client.get("/main-depends/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "q"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "q"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_main_depends_q_foo():
+ response = client.get("/main-depends/?q=foo")
+ assert response.status_code == 200
+ assert response.json() == {
+ "in": "main-depends",
+ "params": {"q": "foo", "skip": 0, "limit": 100},
+ }
+
+
+def test_main_depends_q_foo_skip_100_limit_200():
+ response = client.get("/main-depends/?q=foo&skip=100&limit=200")
+ assert response.status_code == 200
+ assert response.json() == {
+ "in": "main-depends",
+ "params": {"q": "foo", "skip": 100, "limit": 200},
+ }
+
+
+def test_decorator_depends():
+ response = client.get("/decorator-depends/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "q"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "q"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_decorator_depends_q_foo():
+ response = client.get("/decorator-depends/?q=foo")
+ assert response.status_code == 200
+ assert response.json() == {"in": "decorator-depends"}
+
+
+def test_decorator_depends_q_foo_skip_100_limit_200():
+ response = client.get("/decorator-depends/?q=foo&skip=100&limit=200")
+ assert response.status_code == 200
+ assert response.json() == {"in": "decorator-depends"}
+
+
+def test_router_depends():
+ response = client.get("/router-depends/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "q"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "q"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_router_depends_q_foo():
+ response = client.get("/router-depends/?q=foo")
+ assert response.status_code == 200
+ assert response.json() == {
+ "in": "router-depends",
+ "params": {"q": "foo", "skip": 0, "limit": 100},
+ }
+
+
+def test_router_depends_q_foo_skip_100_limit_200():
+ response = client.get("/router-depends/?q=foo&skip=100&limit=200")
+ assert response.status_code == 200
+ assert response.json() == {
+ "in": "router-depends",
+ "params": {"q": "foo", "skip": 100, "limit": 200},
+ }
+
+
+def test_router_decorator_depends():
+ response = client.get("/router-decorator-depends/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "q"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "q"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_router_decorator_depends_q_foo():
+ response = client.get("/router-decorator-depends/?q=foo")
+ assert response.status_code == 200
+ assert response.json() == {"in": "router-decorator-depends"}
+
+
+def test_router_decorator_depends_q_foo_skip_100_limit_200():
+ response = client.get("/router-decorator-depends/?q=foo&skip=100&limit=200")
+ assert response.status_code == 200
+ assert response.json() == {"in": "router-decorator-depends"}
@pytest.mark.parametrize(
app.dependency_overrides = {}
-@pytest.mark.parametrize(
- "url,status_code,expected",
- [
- (
- "/main-depends/",
- 422,
- {
- "detail": [
- {
- "loc": ["query", "k"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- ),
- (
- "/main-depends/?q=foo",
- 422,
- {
- "detail": [
- {
- "loc": ["query", "k"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- ),
- ("/main-depends/?k=bar", 200, {"in": "main-depends", "params": {"k": "bar"}}),
- (
- "/decorator-depends/",
- 422,
- {
- "detail": [
- {
- "loc": ["query", "k"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- ),
- (
- "/decorator-depends/?q=foo",
- 422,
- {
- "detail": [
- {
- "loc": ["query", "k"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- ),
- ("/decorator-depends/?k=bar", 200, {"in": "decorator-depends"}),
- (
- "/router-depends/",
- 422,
- {
- "detail": [
- {
- "loc": ["query", "k"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- ),
- (
- "/router-depends/?q=foo",
- 422,
- {
- "detail": [
- {
- "loc": ["query", "k"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- ),
- (
- "/router-depends/?k=bar",
- 200,
- {"in": "router-depends", "params": {"k": "bar"}},
- ),
- (
- "/router-decorator-depends/",
- 422,
- {
- "detail": [
- {
- "loc": ["query", "k"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- ),
- (
- "/router-decorator-depends/?q=foo",
- 422,
- {
- "detail": [
- {
- "loc": ["query", "k"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- ),
- ("/router-decorator-depends/?k=bar", 200, {"in": "router-decorator-depends"}),
- ],
-)
-def test_override_with_sub(url, status_code, expected):
+def test_override_with_sub_main_depends():
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
- response = client.get(url)
- assert response.status_code == status_code
- assert response.json() == expected
+ response = client.get("/main-depends/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "k"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "k"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+ app.dependency_overrides = {}
+
+
+def test_override_with_sub__main_depends_q_foo():
+ app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
+ response = client.get("/main-depends/?q=foo")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "k"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "k"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+ app.dependency_overrides = {}
+
+
+def test_override_with_sub_main_depends_k_bar():
+ app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
+ response = client.get("/main-depends/?k=bar")
+ assert response.status_code == 200
+ assert response.json() == {"in": "main-depends", "params": {"k": "bar"}}
+ app.dependency_overrides = {}
+
+
+def test_override_with_sub_decorator_depends():
+ app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
+ response = client.get("/decorator-depends/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "k"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "k"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+ app.dependency_overrides = {}
+
+
+def test_override_with_sub_decorator_depends_q_foo():
+ app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
+ response = client.get("/decorator-depends/?q=foo")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "k"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "k"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+ app.dependency_overrides = {}
+
+
+def test_override_with_sub_decorator_depends_k_bar():
+ app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
+ response = client.get("/decorator-depends/?k=bar")
+ assert response.status_code == 200
+ assert response.json() == {"in": "decorator-depends"}
+ app.dependency_overrides = {}
+
+
+def test_override_with_sub_router_depends():
+ app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
+ response = client.get("/router-depends/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "k"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "k"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+ app.dependency_overrides = {}
+
+
+def test_override_with_sub_router_depends_q_foo():
+ app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
+ response = client.get("/router-depends/?q=foo")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "k"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "k"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+ app.dependency_overrides = {}
+
+
+def test_override_with_sub_router_depends_k_bar():
+ app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
+ response = client.get("/router-depends/?k=bar")
+ assert response.status_code == 200
+ assert response.json() == {"in": "router-depends", "params": {"k": "bar"}}
+ app.dependency_overrides = {}
+
+
+def test_override_with_sub_router_decorator_depends():
+ app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
+ response = client.get("/router-decorator-depends/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "k"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "k"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+ app.dependency_overrides = {}
+
+
+def test_override_with_sub_router_decorator_depends_q_foo():
+ app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
+ response = client.get("/router-decorator-depends/?q=foo")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "k"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "k"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+ app.dependency_overrides = {}
+
+
+def test_override_with_sub_router_decorator_depends_k_bar():
+ app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
+ response = client.get("/router-decorator-depends/?k=bar")
+ assert response.status_code == 200
+ assert response.json() == {"in": "router-decorator-depends"}
app.dependency_overrides = {}
from typing import Optional
+from dirty_equals import IsDict
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
- "price": {"title": "Price", "type": "number"},
+ "price": IsDict(
+ {
+ "title": "Price",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ # TODO: remove when deprecating Pydantic v1
+ | IsDict({"title": "Price", "type": "number"}),
},
},
"ValidationError": {
--- /dev/null
+from typing import Optional
+
+from fastapi import Depends, FastAPI
+from pydantic import BaseModel, validator
+
+app = FastAPI()
+
+
+class ModelB(BaseModel):
+ username: str
+
+
+class ModelC(ModelB):
+ password: str
+
+
+class ModelA(BaseModel):
+ name: str
+ description: Optional[str] = None
+ model_b: ModelB
+
+ @validator("name")
+ def lower_username(cls, name: str, values):
+ if not name.endswith("A"):
+ raise ValueError("name must end in A")
+ return name
+
+
+async def get_model_c() -> ModelC:
+ return ModelC(username="test-user", password="test-password")
+
+
+@app.get("/model/{name}", response_model=ModelA)
+async def get_model_a(name: str, model_c=Depends(get_model_c)):
+ return {"name": name, "description": "model-a-desc", "model_b": model_c}
-from typing import Optional
-
import pytest
-from fastapi import Depends, FastAPI
+from fastapi.exceptions import ResponseValidationError
from fastapi.testclient import TestClient
-from pydantic import BaseModel, ValidationError, validator
-
-app = FastAPI()
-
-
-class ModelB(BaseModel):
- username: str
-
-
-class ModelC(ModelB):
- password: str
-
-
-class ModelA(BaseModel):
- name: str
- description: Optional[str] = None
- model_b: ModelB
-
- @validator("name")
- def lower_username(cls, name: str, values):
- if not name.endswith("A"):
- raise ValueError("name must end in A")
- return name
-
-
-async def get_model_c() -> ModelC:
- return ModelC(username="test-user", password="test-password")
+from ..utils import needs_pydanticv1
-@app.get("/model/{name}", response_model=ModelA)
-async def get_model_a(name: str, model_c=Depends(get_model_c)):
- return {"name": name, "description": "model-a-desc", "model_b": model_c}
+@pytest.fixture(name="client")
+def get_client():
+ from .app_pv1 import app
-client = TestClient(app)
+ client = TestClient(app)
+ return client
-def test_filter_sub_model():
+@needs_pydanticv1
+def test_filter_sub_model(client: TestClient):
response = client.get("/model/modelA")
assert response.status_code == 200, response.text
assert response.json() == {
}
-def test_validator_is_cloned():
- with pytest.raises(ValidationError) as err:
+@needs_pydanticv1
+def test_validator_is_cloned(client: TestClient):
+ with pytest.raises(ResponseValidationError) as err:
client.get("/model/modelX")
assert err.value.errors() == [
{
]
-def test_openapi_schema():
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
--- /dev/null
+from typing import Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import Depends, FastAPI
+from fastapi.exceptions import ResponseValidationError
+from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
+
+from .utils import needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from pydantic import BaseModel, FieldValidationInfo, field_validator
+
+ app = FastAPI()
+
+ class ModelB(BaseModel):
+ username: str
+
+ class ModelC(ModelB):
+ password: str
+
+ class ModelA(BaseModel):
+ name: str
+ description: Optional[str] = None
+ foo: ModelB
+
+ @field_validator("name")
+ def lower_username(cls, name: str, info: FieldValidationInfo):
+ if not name.endswith("A"):
+ raise ValueError("name must end in A")
+ return name
+
+ async def get_model_c() -> ModelC:
+ return ModelC(username="test-user", password="test-password")
+
+ @app.get("/model/{name}", response_model=ModelA)
+ async def get_model_a(name: str, model_c=Depends(get_model_c)):
+ return {"name": name, "description": "model-a-desc", "foo": model_c}
+
+ client = TestClient(app)
+ return client
+
+
+@needs_pydanticv2
+def test_filter_sub_model(client: TestClient):
+ response = client.get("/model/modelA")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "name": "modelA",
+ "description": "model-a-desc",
+ "foo": {"username": "test-user"},
+ }
+
+
+@needs_pydanticv2
+def test_validator_is_cloned(client: TestClient):
+ with pytest.raises(ResponseValidationError) as err:
+ client.get("/model/modelX")
+ assert err.value.errors() == [
+ IsDict(
+ {
+ "type": "value_error",
+ "loc": ("response", "name"),
+ "msg": "Value error, name must end in A",
+ "input": "modelX",
+ "ctx": {"error": "name must end in A"},
+ "url": match_pydantic_error_url("value_error"),
+ }
+ )
+ | IsDict(
+ # TODO remove when deprecating Pydantic v1
+ {
+ "loc": ("response", "name"),
+ "msg": "name must end in A",
+ "type": "value_error",
+ }
+ )
+ ]
+
+
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/model/{name}": {
+ "get": {
+ "summary": "Get Model A",
+ "operationId": "get_model_a_model__name__get",
+ "parameters": [
+ {
+ "required": True,
+ "schema": {"title": "Name", "type": "string"},
+ "name": "name",
+ "in": "path",
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/ModelA"}
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ "ModelA": {
+ "title": "ModelA",
+ "required": ["name", "foo"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ |
+ # TODO remove when deprecating Pydantic v1
+ IsDict({"title": "Description", "type": "string"}),
+ "foo": {"$ref": "#/components/schemas/ModelB"},
+ },
+ },
+ "ModelB": {
+ "title": "ModelB",
+ "required": ["username"],
+ "type": "object",
+ "properties": {"username": {"title": "Username", "type": "string"}},
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ }
+ },
+ }
from typing import Optional
+from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
assert response.json() == {"item_id": "item01", "user_id": "abc123"}
-def test_schema_1():
- """Check that the user_id is a required path parameter under /users"""
+def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
- r = response.json()
-
- d = {
- "required": True,
- "schema": {"title": "User Id", "type": "string"},
- "name": "user_id",
- "in": "path",
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/users/": {
+ "get": {
+ "summary": "Get Users",
+ "operationId": "get_users_users__get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ },
+ "/users/{user_id}": {
+ "get": {
+ "summary": "Get User",
+ "operationId": "get_user_users__user_id__get",
+ "parameters": [
+ {
+ "required": True,
+ "schema": {"title": "User Id", "type": "string"},
+ "name": "user_id",
+ "in": "path",
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ "/items/": {
+ "get": {
+ "summary": "Get Items",
+ "operationId": "get_items_items__get",
+ "parameters": [
+ {
+ "required": False,
+ "name": "user_id",
+ "in": "query",
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "User Id",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "User Id", "type": "string"}
+ ),
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ "/items/{item_id}": {
+ "get": {
+ "summary": "Get Item",
+ "operationId": "get_item_items__item_id__get",
+ "parameters": [
+ {
+ "required": True,
+ "schema": {"title": "Item Id", "type": "string"},
+ "name": "item_id",
+ "in": "path",
+ },
+ {
+ "required": False,
+ "name": "user_id",
+ "in": "query",
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "User Id",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "User Id", "type": "string"}
+ ),
+ },
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ "/users/{user_id}/items/": {
+ "get": {
+ "summary": "Get Items",
+ "operationId": "get_items_users__user_id__items__get",
+ "parameters": [
+ {
+ "required": True,
+ "name": "user_id",
+ "in": "path",
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "User Id",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "User Id", "type": "string"}
+ ),
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ "/users/{user_id}/items/{item_id}": {
+ "get": {
+ "summary": "Get Item",
+ "operationId": "get_item_users__user_id__items__item_id__get",
+ "parameters": [
+ {
+ "required": True,
+ "schema": {"title": "Item Id", "type": "string"},
+ "name": "item_id",
+ "in": "path",
+ },
+ {
+ "required": True,
+ "name": "user_id",
+ "in": "path",
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "User Id",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "User Id", "type": "string"}
+ ),
+ },
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ }
+ },
}
-
- assert d in r["paths"]["/users/{user_id}"]["get"]["parameters"]
- assert d in r["paths"]["/users/{user_id}/items/"]["get"]["parameters"]
-
-
-def test_schema_2():
- """Check that the user_id is an optional query parameter under /items"""
- response = client.get("/openapi.json")
- assert response.status_code == 200, response.text
- r = response.json()
-
- d = {
- "required": False,
- "schema": {"title": "User Id", "type": "string"},
- "name": "user_id",
- "in": "query",
- }
-
- assert d in r["paths"]["/items/{item_id}"]["get"]["parameters"]
- assert d in r["paths"]["/items/"]["get"]["parameters"]
from fastapi.testclient import TestClient
from pydantic import BaseModel
-app = FastAPI()
+from .utils import needs_pydanticv1, needs_pydanticv2
class MyUuid:
raise TypeError("vars() argument must have __dict__ attribute")
-@app.get("/fast_uuid")
-def return_fast_uuid():
- # I don't want to import asyncpg for this test so I made my own UUID
- # Import asyncpg and uncomment the two lines below for the actual bug
+@needs_pydanticv2
+def test_pydanticv2():
+ from pydantic import field_serializer
- # from asyncpg.pgproto import pgproto
- # asyncpg_uuid = pgproto.UUID("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
+ app = FastAPI()
- asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
- assert isinstance(asyncpg_uuid, uuid.UUID)
- assert type(asyncpg_uuid) != uuid.UUID
- with pytest.raises(TypeError):
- vars(asyncpg_uuid)
- return {"fast_uuid": asyncpg_uuid}
+ @app.get("/fast_uuid")
+ def return_fast_uuid():
+ asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
+ assert isinstance(asyncpg_uuid, uuid.UUID)
+ assert type(asyncpg_uuid) != uuid.UUID
+ with pytest.raises(TypeError):
+ vars(asyncpg_uuid)
+ return {"fast_uuid": asyncpg_uuid}
+ class SomeCustomClass(BaseModel):
+ model_config = {"arbitrary_types_allowed": True}
-class SomeCustomClass(BaseModel):
- class Config:
- arbitrary_types_allowed = True
- json_encoders = {uuid.UUID: str}
+ a_uuid: MyUuid
- a_uuid: MyUuid
+ @field_serializer("a_uuid")
+ def serialize_a_uuid(self, v):
+ return str(v)
+ @app.get("/get_custom_class")
+ def return_some_user():
+ # Test that the fix also works for custom pydantic classes
+ return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
-@app.get("/get_custom_class")
-def return_some_user():
- # Test that the fix also works for custom pydantic classes
- return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
+ client = TestClient(app)
+ with client:
+ response_simple = client.get("/fast_uuid")
+ response_pydantic = client.get("/get_custom_class")
+
+ assert response_simple.json() == {
+ "fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51"
+ }
+
+ assert response_pydantic.json() == {
+ "a_uuid": "b8799909-f914-42de-91bc-95c819218d01"
+ }
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+def test_pydanticv1():
+ app = FastAPI()
+
+ @app.get("/fast_uuid")
+ def return_fast_uuid():
+ asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
+ assert isinstance(asyncpg_uuid, uuid.UUID)
+ assert type(asyncpg_uuid) != uuid.UUID
+ with pytest.raises(TypeError):
+ vars(asyncpg_uuid)
+ return {"fast_uuid": asyncpg_uuid}
+
+ class SomeCustomClass(BaseModel):
+ class Config:
+ arbitrary_types_allowed = True
+ json_encoders = {uuid.UUID: str}
+
+ a_uuid: MyUuid
-client = TestClient(app)
+ @app.get("/get_custom_class")
+ def return_some_user():
+ # Test that the fix also works for custom pydantic classes
+ return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
+ client = TestClient(app)
-def test_dt():
with client:
response_simple = client.get("/fast_uuid")
response_pydantic = client.get("/get_custom_class")
from collections import deque
from dataclasses import dataclass
from datetime import datetime, timezone
+from decimal import Decimal
from enum import Enum
from pathlib import PurePath, PurePosixPath, PureWindowsPath
from typing import Optional
import pytest
+from fastapi._compat import PYDANTIC_V2
from fastapi.encoders import jsonable_encoder
-from pydantic import BaseModel, Field, ValidationError, create_model
+from pydantic import BaseModel, Field, ValidationError
+
+from .utils import needs_pydanticv1, needs_pydanticv2
class Person:
raise NotImplementedError()
-class ModelWithCustomEncoder(BaseModel):
- dt_field: datetime
-
- class Config:
- json_encoders = {
- datetime: lambda dt: dt.replace(
- microsecond=0, tzinfo=timezone.utc
- ).isoformat()
- }
-
-
-class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
- class Config:
- pass
-
-
class RoleEnum(Enum):
admin = "admin"
normal = "normal"
class ModelWithConfig(BaseModel):
role: Optional[RoleEnum] = None
- class Config:
- use_enum_values = True
+ if PYDANTIC_V2:
+ model_config = {"use_enum_values": True}
+ else:
+
+ class Config:
+ use_enum_values = True
class ModelWithAlias(BaseModel):
bla: str = "bla"
-class ModelWithRoot(BaseModel):
- __root__: str
-
-
-@pytest.fixture(
- name="model_with_path", params=[PurePath, PurePosixPath, PureWindowsPath]
-)
-def fixture_model_with_path(request):
- class Config:
- arbitrary_types_allowed = True
-
- ModelWithPath = create_model(
- "ModelWithPath", path=(request.param, ...), __config__=Config # type: ignore
- )
- return ModelWithPath(path=request.param("/foo", "bar"))
-
-
def test_encode_dict():
pet = {"name": "Firulais", "owner": {"name": "Foo"}}
assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}}
jsonable_encoder(unserializable)
-def test_encode_custom_json_encoders_model():
+@needs_pydanticv2
+def test_encode_custom_json_encoders_model_pydanticv2():
+ from pydantic import field_serializer
+
+ class ModelWithCustomEncoder(BaseModel):
+ dt_field: datetime
+
+ @field_serializer("dt_field")
+ def serialize_dt_field(self, dt):
+ return dt.replace(microsecond=0, tzinfo=timezone.utc).isoformat()
+
+ class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
+ pass
+
model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
+ subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
+ assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
+
+
+# TODO: remove when deprecating Pydantic v1
+@needs_pydanticv1
+def test_encode_custom_json_encoders_model_pydanticv1():
+ class ModelWithCustomEncoder(BaseModel):
+ dt_field: datetime
+ class Config:
+ json_encoders = {
+ datetime: lambda dt: dt.replace(
+ microsecond=0, tzinfo=timezone.utc
+ ).isoformat()
+ }
-def test_encode_custom_json_encoders_model_subclass():
- model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
+ class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder):
+ class Config:
+ pass
+
+ model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
+ subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
+ assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
def test_encode_model_with_config():
}
+@needs_pydanticv1
def test_custom_encoders():
class safe_datetime(datetime):
pass
assert encoded_instance == custom_enum_encoder(instance)
-def test_encode_model_with_path(model_with_path):
- if isinstance(model_with_path.path, PureWindowsPath):
- expected = "\\foo\\bar"
- else:
- expected = "/foo/bar"
- assert jsonable_encoder(model_with_path) == {"path": expected}
+def test_encode_model_with_pure_path():
+ class ModelWithPath(BaseModel):
+ path: PurePath
+
+ if PYDANTIC_V2:
+ model_config = {"arbitrary_types_allowed": True}
+ else:
+
+ class Config:
+ arbitrary_types_allowed = True
+
+ obj = ModelWithPath(path=PurePath("/foo", "bar"))
+ assert jsonable_encoder(obj) == {"path": "/foo/bar"}
+
+
+def test_encode_model_with_pure_posix_path():
+ class ModelWithPath(BaseModel):
+ path: PurePosixPath
+
+ if PYDANTIC_V2:
+ model_config = {"arbitrary_types_allowed": True}
+ else:
+
+ class Config:
+ arbitrary_types_allowed = True
+
+ obj = ModelWithPath(path=PurePosixPath("/foo", "bar"))
+ assert jsonable_encoder(obj) == {"path": "/foo/bar"}
+def test_encode_model_with_pure_windows_path():
+ class ModelWithPath(BaseModel):
+ path: PureWindowsPath
+
+ if PYDANTIC_V2:
+ model_config = {"arbitrary_types_allowed": True}
+ else:
+
+ class Config:
+ arbitrary_types_allowed = True
+
+ obj = ModelWithPath(path=PureWindowsPath("/foo", "bar"))
+ assert jsonable_encoder(obj) == {"path": "\\foo\\bar"}
+
+
+@needs_pydanticv1
def test_encode_root():
+ class ModelWithRoot(BaseModel):
+ __root__: str
+
model = ModelWithRoot(__root__="Foo")
assert jsonable_encoder(model) == "Foo"
+@needs_pydanticv2
+def test_decimal_encoder_float():
+ data = {"value": Decimal(1.23)}
+ assert jsonable_encoder(data) == {"value": 1.23}
+
+
+@needs_pydanticv2
+def test_decimal_encoder_int():
+ data = {"value": Decimal(2)}
+ assert jsonable_encoder(data) == {"value": 2}
+
+
def test_encode_deque_encodes_child_models():
class Model(BaseModel):
test: str
from decimal import Decimal
from typing import List
+from dirty_equals import IsDict, IsOneOf
from fastapi import FastAPI
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel, condecimal
app = FastAPI()
client = TestClient(app)
-single_error = {
- "detail": [
- {
- "ctx": {"limit_value": 0.0},
- "loc": ["body", 0, "age"],
- "msg": "ensure this value is greater than 0",
- "type": "value_error.number.not_gt",
- }
- ]
-}
-
-multiple_errors = {
- "detail": [
- {
- "loc": ["body", 0, "name"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", 0, "age"],
- "msg": "value is not a valid decimal",
- "type": "type_error.decimal",
- },
- {
- "loc": ["body", 1, "name"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", 1, "age"],
- "msg": "value is not a valid decimal",
- "type": "type_error.decimal",
- },
- ]
-}
-
-
def test_put_correct_body():
response = client.post("/items/", json=[{"name": "Foo", "age": 5}])
assert response.status_code == 200, response.text
- assert response.json() == {"item": [{"name": "Foo", "age": 5}]}
+ assert response.json() == {
+ "item": [
+ {
+ "name": "Foo",
+ "age": IsOneOf(
+ 5,
+ # TODO: remove when deprecating Pydantic v1
+ "5",
+ ),
+ }
+ ]
+ }
def test_jsonable_encoder_requiring_error():
response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}])
assert response.status_code == 422, response.text
- assert response.json() == single_error
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "greater_than",
+ "loc": ["body", 0, "age"],
+ "msg": "Input should be greater than 0",
+ "input": -1.0,
+ "ctx": {"gt": 0.0},
+ "url": match_pydantic_error_url("greater_than"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "ctx": {"limit_value": 0.0},
+ "loc": ["body", 0, "age"],
+ "msg": "ensure this value is greater than 0",
+ "type": "value_error.number.not_gt",
+ }
+ ]
+ }
+ )
def test_put_incorrect_body_multiple():
response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}])
assert response.status_code == 422, response.text
- assert response.json() == multiple_errors
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", 0, "name"],
+ "msg": "Field required",
+ "input": {"age": "five"},
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "decimal_parsing",
+ "loc": ["body", 0, "age"],
+ "msg": "Input should be a valid decimal",
+ "input": "five",
+ },
+ {
+ "type": "missing",
+ "loc": ["body", 1, "name"],
+ "msg": "Field required",
+ "input": {"age": "six"},
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "decimal_parsing",
+ "loc": ["body", 1, "age"],
+ "msg": "Input should be a valid decimal",
+ "input": "six",
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", 0, "name"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", 0, "age"],
+ "msg": "value is not a valid decimal",
+ "type": "type_error.decimal",
+ },
+ {
+ "loc": ["body", 1, "name"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", 1, "age"],
+ "msg": "value is not a valid decimal",
+ "type": "type_error.decimal",
+ },
+ ]
+ }
+ )
def test_openapi_schema():
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
- "age": {
- "title": "Age",
- "exclusiveMinimum": 0.0,
- "type": "number",
- },
+ "age": IsDict(
+ {
+ "title": "Age",
+ "anyOf": [
+ {"exclusiveMinimum": 0.0, "type": "number"},
+ {"type": "string"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Age",
+ "exclusiveMinimum": 0.0,
+ "type": "number",
+ }
+ ),
},
},
"ValidationError": {
from typing import List
+from dirty_equals import IsDict
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
app = FastAPI()
client = TestClient(app)
-multiple_errors = {
- "detail": [
- {
- "loc": ["query", "q", 0],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
- },
- {
- "loc": ["query", "q", 1],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
- },
- ]
-}
-
-
def test_multi_query():
response = client.get("/items/?q=5&q=6")
assert response.status_code == 200, response.text
def test_multi_query_incorrect():
response = client.get("/items/?q=five&q=six")
assert response.status_code == 422, response.text
- assert response.json() == multiple_errors
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["query", "q", 0],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "five",
+ "url": match_pydantic_error_url("int_parsing"),
+ },
+ {
+ "type": "int_parsing",
+ "loc": ["query", "q", 1],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "six",
+ "url": match_pydantic_error_url("int_parsing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "q", 0],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ },
+ {
+ "loc": ["query", "q", 1],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ },
+ ]
+ }
+ )
def test_openapi_schema():
from typing import Optional
+from dirty_equals import IsDict
from fastapi import FastAPI
from fastapi.testclient import TestClient
"parameters": [
{
"required": False,
- "schema": {
- "title": "Standard Query Param",
- "type": "integer",
- "default": 50,
- },
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "integer"}, {"type": "null"}],
+ "default": 50,
+ "title": "Standard Query Param",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Standard Query Param",
+ "type": "integer",
+ "default": 50,
+ }
+ ),
"name": "standard_query_param",
"in": "query",
},
+from dirty_equals import IsOneOf
from fastapi import FastAPI
from fastapi.testclient import TestClient
"servers": [
{"url": "/", "description": "Default, relative server"},
{
- "url": "http://staging.localhost.tiangolo.com:8000",
+ "url": IsOneOf(
+ "http://staging.localhost.tiangolo.com:8000/",
+ # TODO: remove when deprecating Pydantic v1
+ "http://staging.localhost.tiangolo.com:8000",
+ ),
"description": "Staging but actually localhost still",
},
- {"url": "https://prod.example.com"},
+ {
+ "url": IsOneOf(
+ "https://prod.example.com/",
+ # TODO: remove when deprecating Pydantic v1
+ "https://prod.example.com",
+ )
+ },
],
"paths": {
"/foo": {
from typing import Any, List
-import pytest
+from dirty_equals import IsOneOf
from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query
test_data: List[Any] = ["teststr", None, ..., 1, []]
return {} # pragma: no cover
-@pytest.fixture(scope="function", params=test_data)
-def params(request):
- return request.param
+def test_param_repr_str():
+ assert repr(Param("teststr")) == "Param(teststr)"
-def test_param_repr(params):
- assert repr(Param(params)) == "Param(" + str(params) + ")"
+def test_param_repr_none():
+ assert repr(Param(None)) == "Param(None)"
+
+
+def test_param_repr_ellipsis():
+ assert repr(Param(...)) == IsOneOf(
+ "Param(PydanticUndefined)",
+ # TODO: remove when deprecating Pydantic v1
+ "Param(Ellipsis)",
+ )
+
+
+def test_param_repr_number():
+ assert repr(Param(1)) == "Param(1)"
+
+
+def test_param_repr_list():
+ assert repr(Param([])) == "Param([])"
def test_path_repr():
- assert repr(Path()) == "Path(Ellipsis)"
- assert repr(Path(...)) == "Path(Ellipsis)"
+ assert repr(Path()) == IsOneOf(
+ "Path(PydanticUndefined)",
+ # TODO: remove when deprecating Pydantic v1
+ "Path(Ellipsis)",
+ )
+ assert repr(Path(...)) == IsOneOf(
+ "Path(PydanticUndefined)",
+ # TODO: remove when deprecating Pydantic v1
+ "Path(Ellipsis)",
+ )
-def test_query_repr(params):
- assert repr(Query(params)) == "Query(" + str(params) + ")"
+def test_query_repr_str():
+ assert repr(Query("teststr")) == "Query(teststr)"
-def test_header_repr(params):
- assert repr(Header(params)) == "Header(" + str(params) + ")"
+def test_query_repr_none():
+ assert repr(Query(None)) == "Query(None)"
+
+
+def test_query_repr_ellipsis():
+ assert repr(Query(...)) == IsOneOf(
+ "Query(PydanticUndefined)",
+ # TODO: remove when deprecating Pydantic v1
+ "Query(Ellipsis)",
+ )
+
+
+def test_query_repr_number():
+ assert repr(Query(1)) == "Query(1)"
+
+
+def test_query_repr_list():
+ assert repr(Query([])) == "Query([])"
+
+
+def test_header_repr_str():
+ assert repr(Header("teststr")) == "Header(teststr)"
+
+
+def test_header_repr_none():
+ assert repr(Header(None)) == "Header(None)"
+
+
+def test_header_repr_ellipsis():
+ assert repr(Header(...)) == IsOneOf(
+ "Header(PydanticUndefined)",
+ # TODO: remove when deprecating Pydantic v1
+ "Header(Ellipsis)",
+ )
+
+
+def test_header_repr_number():
+ assert repr(Header(1)) == "Header(1)"
+
+
+def test_header_repr_list():
+ assert repr(Header([])) == "Header([])"
+
+
+def test_cookie_repr_str():
+ assert repr(Cookie("teststr")) == "Cookie(teststr)"
+
+
+def test_cookie_repr_none():
+ assert repr(Cookie(None)) == "Cookie(None)"
+
+
+def test_cookie_repr_ellipsis():
+ assert repr(Cookie(...)) == IsOneOf(
+ "Cookie(PydanticUndefined)",
+ # TODO: remove when deprecating Pydantic v1
+ "Cookie(Ellipsis)",
+ )
+
+
+def test_cookie_repr_number():
+ assert repr(Cookie(1)) == "Cookie(1)"
+
+
+def test_cookie_repr_list():
+ assert repr(Cookie([])) == "Cookie([])"
+
+
+def test_body_repr_str():
+ assert repr(Body("teststr")) == "Body(teststr)"
+
+
+def test_body_repr_none():
+ assert repr(Body(None)) == "Body(None)"
+
+
+def test_body_repr_ellipsis():
+ assert repr(Body(...)) == IsOneOf(
+ "Body(PydanticUndefined)",
+ # TODO: remove when deprecating Pydantic v1
+ "Body(Ellipsis)",
+ )
-def test_cookie_repr(params):
- assert repr(Cookie(params)) == "Cookie(" + str(params) + ")"
+def test_body_repr_number():
+ assert repr(Body(1)) == "Body(1)"
-def test_body_repr(params):
- assert repr(Body(params)) == "Body(" + str(params) + ")"
+def test_body_repr_list():
+ assert repr(Body([])) == "Body([])"
def test_depends_repr():
-import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from .main import app
assert response.json() == {"detail": "Not Found"}
-response_not_valid_bool = {
- "detail": [
+def test_path_foobar():
+ response = client.get("/path/foobar")
+ assert response.status_code == 200
+ assert response.json() == "foobar"
+
+
+def test_path_str_foobar():
+ response = client.get("/path/str/foobar")
+ assert response.status_code == 200
+ assert response.json() == "foobar"
+
+
+def test_path_str_42():
+ response = client.get("/path/str/42")
+ assert response.status_code == 200
+ assert response.json() == "42"
+
+
+def test_path_str_True():
+ response = client.get("/path/str/True")
+ assert response.status_code == 200
+ assert response.json() == "True"
+
+
+def test_path_int_foobar():
+ response = client.get("/path/int/foobar")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["path", "item_id"],
- "msg": "value could not be parsed to a boolean",
- "type": "type_error.bool",
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "foobar",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
}
- ]
-}
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
-response_not_valid_int = {
- "detail": [
+
+def test_path_int_True():
+ response = client.get("/path/int/True")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "True",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["path", "item_id"],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
}
- ]
-}
+ )
+
+
+def test_path_int_42():
+ response = client.get("/path/int/42")
+ assert response.status_code == 200
+ assert response.json() == 42
+
-response_not_valid_float = {
- "detail": [
+def test_path_int_42_5():
+ response = client.get("/path/int/42.5")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["path", "item_id"],
- "msg": "value is not a valid float",
- "type": "type_error.float",
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "42.5",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
}
- ]
-}
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
-response_at_least_3 = {
- "detail": [
+
+def test_path_float_foobar():
+ response = client.get("/path/float/foobar")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "float_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid number, unable to parse string as a number",
+ "input": "foobar",
+ "url": match_pydantic_error_url("float_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["path", "item_id"],
- "msg": "ensure this value has at least 3 characters",
- "type": "value_error.any_str.min_length",
- "ctx": {"limit_value": 3},
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid float",
+ "type": "type_error.float",
+ }
+ ]
}
- ]
-}
+ )
-response_at_least_2 = {
- "detail": [
+def test_path_float_True():
+ response = client.get("/path/float/True")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "float_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid number, unable to parse string as a number",
+ "input": "True",
+ "url": match_pydantic_error_url("float_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["path", "item_id"],
- "msg": "ensure this value has at least 2 characters",
- "type": "value_error.any_str.min_length",
- "ctx": {"limit_value": 2},
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid float",
+ "type": "type_error.float",
+ }
+ ]
}
- ]
-}
+ )
+
+def test_path_float_42():
+ response = client.get("/path/float/42")
+ assert response.status_code == 200
+ assert response.json() == 42
-response_maximum_3 = {
- "detail": [
+
+def test_path_float_42_5():
+ response = client.get("/path/float/42.5")
+ assert response.status_code == 200
+ assert response.json() == 42.5
+
+
+def test_path_bool_foobar():
+ response = client.get("/path/bool/foobar")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "bool_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid boolean, unable to interpret input",
+ "input": "foobar",
+ "url": match_pydantic_error_url("bool_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["path", "item_id"],
- "msg": "ensure this value has at most 3 characters",
- "type": "value_error.any_str.max_length",
- "ctx": {"limit_value": 3},
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value could not be parsed to a boolean",
+ "type": "type_error.bool",
+ }
+ ]
}
- ]
-}
+ )
+
+def test_path_bool_True():
+ response = client.get("/path/bool/True")
+ assert response.status_code == 200
+ assert response.json() is True
-response_greater_than_3 = {
- "detail": [
+
+def test_path_bool_42():
+ response = client.get("/path/bool/42")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["path", "item_id"],
- "msg": "ensure this value is greater than 3",
- "type": "value_error.number.not_gt",
- "ctx": {"limit_value": 3},
+ "detail": [
+ {
+ "type": "bool_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid boolean, unable to interpret input",
+ "input": "42",
+ "url": match_pydantic_error_url("bool_parsing"),
+ }
+ ]
}
- ]
-}
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value could not be parsed to a boolean",
+ "type": "type_error.bool",
+ }
+ ]
+ }
+ )
+
+
+def test_path_bool_42_5():
+ response = client.get("/path/bool/42.5")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "bool_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid boolean, unable to interpret input",
+ "input": "42.5",
+ "url": match_pydantic_error_url("bool_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value could not be parsed to a boolean",
+ "type": "type_error.bool",
+ }
+ ]
+ }
+ )
+
+
+def test_path_bool_1():
+ response = client.get("/path/bool/1")
+ assert response.status_code == 200
+ assert response.json() is True
+
+
+def test_path_bool_0():
+ response = client.get("/path/bool/0")
+ assert response.status_code == 200
+ assert response.json() is False
+
+
+def test_path_bool_true():
+ response = client.get("/path/bool/true")
+ assert response.status_code == 200
+ assert response.json() is True
+
+
+def test_path_bool_False():
+ response = client.get("/path/bool/False")
+ assert response.status_code == 200
+ assert response.json() is False
+
+def test_path_bool_false():
+ response = client.get("/path/bool/false")
+ assert response.status_code == 200
+ assert response.json() is False
-response_greater_than_0 = {
- "detail": [
+
+def test_path_param_foo():
+ response = client.get("/path/param/foo")
+ assert response.status_code == 200
+ assert response.json() == "foo"
+
+
+def test_path_param_minlength_foo():
+ response = client.get("/path/param-minlength/foo")
+ assert response.status_code == 200
+ assert response.json() == "foo"
+
+
+def test_path_param_minlength_fo():
+ response = client.get("/path/param-minlength/fo")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "string_too_short",
+ "loc": ["path", "item_id"],
+ "msg": "String should have at least 3 characters",
+ "input": "fo",
+ "ctx": {"min_length": 3},
+ "url": match_pydantic_error_url("string_too_short"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value has at least 3 characters",
+ "type": "value_error.any_str.min_length",
+ "ctx": {"limit_value": 3},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_maxlength_foo():
+ response = client.get("/path/param-maxlength/foo")
+ assert response.status_code == 200
+ assert response.json() == "foo"
+
+
+def test_path_param_maxlength_foobar():
+ response = client.get("/path/param-maxlength/foobar")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "string_too_long",
+ "loc": ["path", "item_id"],
+ "msg": "String should have at most 3 characters",
+ "input": "foobar",
+ "ctx": {"max_length": 3},
+ "url": match_pydantic_error_url("string_too_long"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value has at most 3 characters",
+ "type": "value_error.any_str.max_length",
+ "ctx": {"limit_value": 3},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_min_maxlength_foo():
+ response = client.get("/path/param-min_maxlength/foo")
+ assert response.status_code == 200
+ assert response.json() == "foo"
+
+
+def test_path_param_min_maxlength_foobar():
+ response = client.get("/path/param-min_maxlength/foobar")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "string_too_long",
+ "loc": ["path", "item_id"],
+ "msg": "String should have at most 3 characters",
+ "input": "foobar",
+ "ctx": {"max_length": 3},
+ "url": match_pydantic_error_url("string_too_long"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value has at most 3 characters",
+ "type": "value_error.any_str.max_length",
+ "ctx": {"limit_value": 3},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_min_maxlength_f():
+ response = client.get("/path/param-min_maxlength/f")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "string_too_short",
+ "loc": ["path", "item_id"],
+ "msg": "String should have at least 2 characters",
+ "input": "f",
+ "ctx": {"min_length": 2},
+ "url": match_pydantic_error_url("string_too_short"),
+ }
+ ]
+ }
+ ) | IsDict(
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value has at least 2 characters",
+ "type": "value_error.any_str.min_length",
+ "ctx": {"limit_value": 2},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_gt_42():
+ response = client.get("/path/param-gt/42")
+ assert response.status_code == 200
+ assert response.json() == 42
+
+
+def test_path_param_gt_2():
+ response = client.get("/path/param-gt/2")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "greater_than",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be greater than 3",
+ "input": "2",
+ "ctx": {"gt": 3.0},
+ "url": match_pydantic_error_url("greater_than"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value is greater than 3",
+ "type": "value_error.number.not_gt",
+ "ctx": {"limit_value": 3},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_gt0_0_05():
+ response = client.get("/path/param-gt0/0.05")
+ assert response.status_code == 200
+ assert response.json() == 0.05
+
+
+def test_path_param_gt0_0():
+ response = client.get("/path/param-gt0/0")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "greater_than",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be greater than 0",
+ "input": "0",
+ "ctx": {"gt": 0.0},
+ "url": match_pydantic_error_url("greater_than"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value is greater than 0",
+ "type": "value_error.number.not_gt",
+ "ctx": {"limit_value": 0},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_ge_42():
+ response = client.get("/path/param-ge/42")
+ assert response.status_code == 200
+ assert response.json() == 42
+
+
+def test_path_param_ge_3():
+ response = client.get("/path/param-ge/3")
+ assert response.status_code == 200
+ assert response.json() == 3
+
+
+def test_path_param_ge_2():
+ response = client.get("/path/param-ge/2")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "greater_than_equal",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be greater than or equal to 3",
+ "input": "2",
+ "ctx": {"ge": 3.0},
+ "url": match_pydantic_error_url("greater_than_equal"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value is greater than or equal to 3",
+ "type": "value_error.number.not_ge",
+ "ctx": {"limit_value": 3},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_lt_42():
+ response = client.get("/path/param-lt/42")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "less_than",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be less than 3",
+ "input": "42",
+ "ctx": {"lt": 3.0},
+ "url": match_pydantic_error_url("less_than"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value is less than 3",
+ "type": "value_error.number.not_lt",
+ "ctx": {"limit_value": 3},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_lt_2():
+ response = client.get("/path/param-lt/2")
+ assert response.status_code == 200
+ assert response.json() == 2
+
+
+def test_path_param_lt0__1():
+ response = client.get("/path/param-lt0/-1")
+ assert response.status_code == 200
+ assert response.json() == -1
+
+
+def test_path_param_lt0_0():
+ response = client.get("/path/param-lt0/0")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "less_than",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be less than 0",
+ "input": "0",
+ "ctx": {"lt": 0.0},
+ "url": match_pydantic_error_url("less_than"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value is less than 0",
+ "type": "value_error.number.not_lt",
+ "ctx": {"limit_value": 0},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_le_42():
+ response = client.get("/path/param-le/42")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "less_than_equal",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be less than or equal to 3",
+ "input": "42",
+ "ctx": {"le": 3.0},
+ "url": match_pydantic_error_url("less_than_equal"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["path", "item_id"],
- "msg": "ensure this value is greater than 0",
- "type": "value_error.number.not_gt",
- "ctx": {"limit_value": 0},
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value is less than or equal to 3",
+ "type": "value_error.number.not_le",
+ "ctx": {"limit_value": 3},
+ }
+ ]
}
- ]
-}
+ )
-response_greater_than_1 = {
- "detail": [
+def test_path_param_le_3():
+ response = client.get("/path/param-le/3")
+ assert response.status_code == 200
+ assert response.json() == 3
+
+
+def test_path_param_le_2():
+ response = client.get("/path/param-le/2")
+ assert response.status_code == 200
+ assert response.json() == 2
+
+
+def test_path_param_lt_gt_2():
+ response = client.get("/path/param-lt-gt/2")
+ assert response.status_code == 200
+ assert response.json() == 2
+
+
+def test_path_param_lt_gt_4():
+ response = client.get("/path/param-lt-gt/4")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "less_than",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be less than 3",
+ "input": "4",
+ "ctx": {"lt": 3.0},
+ "url": match_pydantic_error_url("less_than"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value is less than 3",
+ "type": "value_error.number.not_lt",
+ "ctx": {"limit_value": 3},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_lt_gt_0():
+ response = client.get("/path/param-lt-gt/0")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "greater_than",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be greater than 1",
+ "input": "0",
+ "ctx": {"gt": 1.0},
+ "url": match_pydantic_error_url("greater_than"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value is greater than 1",
+ "type": "value_error.number.not_gt",
+ "ctx": {"limit_value": 1},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_le_ge_2():
+ response = client.get("/path/param-le-ge/2")
+ assert response.status_code == 200
+ assert response.json() == 2
+
+
+def test_path_param_le_ge_1():
+ response = client.get("/path/param-le-ge/1")
+ assert response.status_code == 200
+
+
+def test_path_param_le_ge_3():
+ response = client.get("/path/param-le-ge/3")
+ assert response.status_code == 200
+ assert response.json() == 3
+
+
+def test_path_param_le_ge_4():
+ response = client.get("/path/param-le-ge/4")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "less_than_equal",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be less than or equal to 3",
+ "input": "4",
+ "ctx": {"le": 3.0},
+ "url": match_pydantic_error_url("less_than_equal"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value is less than or equal to 3",
+ "type": "value_error.number.not_le",
+ "ctx": {"limit_value": 3},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_lt_int_2():
+ response = client.get("/path/param-lt-int/2")
+ assert response.status_code == 200
+ assert response.json() == 2
+
+
+def test_path_param_lt_int_42():
+ response = client.get("/path/param-lt-int/42")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "less_than",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be less than 3",
+ "input": "42",
+ "ctx": {"lt": 3},
+ "url": match_pydantic_error_url("less_than"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value is less than 3",
+ "type": "value_error.number.not_lt",
+ "ctx": {"limit_value": 3},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_lt_int_2_7():
+ response = client.get("/path/param-lt-int/2.7")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "2.7",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_gt_int_42():
+ response = client.get("/path/param-gt-int/42")
+ assert response.status_code == 200
+ assert response.json() == 42
+
+
+def test_path_param_gt_int_2():
+ response = client.get("/path/param-gt-int/2")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "greater_than",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be greater than 3",
+ "input": "2",
+ "ctx": {"gt": 3},
+ "url": match_pydantic_error_url("greater_than"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value is greater than 3",
+ "type": "value_error.number.not_gt",
+ "ctx": {"limit_value": 3},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_gt_int_2_7():
+ response = client.get("/path/param-gt-int/2.7")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "2.7",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_le_int_42():
+ response = client.get("/path/param-le-int/42")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "less_than_equal",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be less than or equal to 3",
+ "input": "42",
+ "ctx": {"le": 3},
+ "url": match_pydantic_error_url("less_than_equal"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value is less than or equal to 3",
+ "type": "value_error.number.not_le",
+ "ctx": {"limit_value": 3},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_le_int_3():
+ response = client.get("/path/param-le-int/3")
+ assert response.status_code == 200
+ assert response.json() == 3
+
+
+def test_path_param_le_int_2():
+ response = client.get("/path/param-le-int/2")
+ assert response.status_code == 200
+ assert response.json() == 2
+
+
+def test_path_param_le_int_2_7():
+ response = client.get("/path/param-le-int/2.7")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "2.7",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_ge_int_42():
+ response = client.get("/path/param-ge-int/42")
+ assert response.status_code == 200
+ assert response.json() == 42
+
+
+def test_path_param_ge_int_3():
+ response = client.get("/path/param-ge-int/3")
+ assert response.status_code == 200
+ assert response.json() == 3
+
+
+def test_path_param_ge_int_2():
+ response = client.get("/path/param-ge-int/2")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "greater_than_equal",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be greater than or equal to 3",
+ "input": "2",
+ "ctx": {"ge": 3},
+ "url": match_pydantic_error_url("greater_than_equal"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value is greater than or equal to 3",
+ "type": "value_error.number.not_ge",
+ "ctx": {"limit_value": 3},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_ge_int_2_7():
+ response = client.get("/path/param-ge-int/2.7")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "2.7",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_lt_gt_int_2():
+ response = client.get("/path/param-lt-gt-int/2")
+ assert response.status_code == 200
+ assert response.json() == 2
+
+
+def test_path_param_lt_gt_int_4():
+ response = client.get("/path/param-lt-gt-int/4")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "less_than",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be less than 3",
+ "input": "4",
+ "ctx": {"lt": 3},
+ "url": match_pydantic_error_url("less_than"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value is less than 3",
+ "type": "value_error.number.not_lt",
+ "ctx": {"limit_value": 3},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_lt_gt_int_0():
+ response = client.get("/path/param-lt-gt-int/0")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "greater_than",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be greater than 1",
+ "input": "0",
+ "ctx": {"gt": 1},
+ "url": match_pydantic_error_url("greater_than"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value is greater than 1",
+ "type": "value_error.number.not_gt",
+ "ctx": {"limit_value": 1},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_lt_gt_int_2_7():
+ response = client.get("/path/param-lt-gt-int/2.7")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "2.7",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_le_ge_int_2():
+ response = client.get("/path/param-le-ge-int/2")
+ assert response.status_code == 200
+ assert response.json() == 2
+
+
+def test_path_param_le_ge_int_1():
+ response = client.get("/path/param-le-ge-int/1")
+ assert response.status_code == 200
+ assert response.json() == 1
+
+
+def test_path_param_le_ge_int_3():
+ response = client.get("/path/param-le-ge-int/3")
+ assert response.status_code == 200
+ assert response.json() == 3
+
+
+def test_path_param_le_ge_int_4():
+ response = client.get("/path/param-le-ge-int/4")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "less_than_equal",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be less than or equal to 3",
+ "input": "4",
+ "ctx": {"le": 3},
+ "url": match_pydantic_error_url("less_than_equal"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "ensure this value is less than or equal to 3",
+ "type": "value_error.number.not_le",
+ "ctx": {"limit_value": 3},
+ }
+ ]
+ }
+ )
+
+
+def test_path_param_le_ge_int_2_7():
+ response = client.get("/path/param-le-ge-int/2.7")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "2.7",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["path", "item_id"],
- "msg": "ensure this value is greater than 1",
- "type": "value_error.number.not_gt",
- "ctx": {"limit_value": 1},
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
}
- ]
-}
-
-
-response_greater_than_equal_3 = {
- "detail": [
- {
- "loc": ["path", "item_id"],
- "msg": "ensure this value is greater than or equal to 3",
- "type": "value_error.number.not_ge",
- "ctx": {"limit_value": 3},
- }
- ]
-}
-
-
-response_less_than_3 = {
- "detail": [
- {
- "loc": ["path", "item_id"],
- "msg": "ensure this value is less than 3",
- "type": "value_error.number.not_lt",
- "ctx": {"limit_value": 3},
- }
- ]
-}
-
-
-response_less_than_0 = {
- "detail": [
- {
- "loc": ["path", "item_id"],
- "msg": "ensure this value is less than 0",
- "type": "value_error.number.not_lt",
- "ctx": {"limit_value": 0},
- }
- ]
-}
-
-
-response_less_than_equal_3 = {
- "detail": [
- {
- "loc": ["path", "item_id"],
- "msg": "ensure this value is less than or equal to 3",
- "type": "value_error.number.not_le",
- "ctx": {"limit_value": 3},
- }
- ]
-}
-
-
-@pytest.mark.parametrize(
- "path,expected_status,expected_response",
- [
- ("/path/foobar", 200, "foobar"),
- ("/path/str/foobar", 200, "foobar"),
- ("/path/str/42", 200, "42"),
- ("/path/str/True", 200, "True"),
- ("/path/int/foobar", 422, response_not_valid_int),
- ("/path/int/True", 422, response_not_valid_int),
- ("/path/int/42", 200, 42),
- ("/path/int/42.5", 422, response_not_valid_int),
- ("/path/float/foobar", 422, response_not_valid_float),
- ("/path/float/True", 422, response_not_valid_float),
- ("/path/float/42", 200, 42),
- ("/path/float/42.5", 200, 42.5),
- ("/path/bool/foobar", 422, response_not_valid_bool),
- ("/path/bool/True", 200, True),
- ("/path/bool/42", 422, response_not_valid_bool),
- ("/path/bool/42.5", 422, response_not_valid_bool),
- ("/path/bool/1", 200, True),
- ("/path/bool/0", 200, False),
- ("/path/bool/true", 200, True),
- ("/path/bool/False", 200, False),
- ("/path/bool/false", 200, False),
- ("/path/param/foo", 200, "foo"),
- ("/path/param-minlength/foo", 200, "foo"),
- ("/path/param-minlength/fo", 422, response_at_least_3),
- ("/path/param-maxlength/foo", 200, "foo"),
- ("/path/param-maxlength/foobar", 422, response_maximum_3),
- ("/path/param-min_maxlength/foo", 200, "foo"),
- ("/path/param-min_maxlength/foobar", 422, response_maximum_3),
- ("/path/param-min_maxlength/f", 422, response_at_least_2),
- ("/path/param-gt/42", 200, 42),
- ("/path/param-gt/2", 422, response_greater_than_3),
- ("/path/param-gt0/0.05", 200, 0.05),
- ("/path/param-gt0/0", 422, response_greater_than_0),
- ("/path/param-ge/42", 200, 42),
- ("/path/param-ge/3", 200, 3),
- ("/path/param-ge/2", 422, response_greater_than_equal_3),
- ("/path/param-lt/42", 422, response_less_than_3),
- ("/path/param-lt/2", 200, 2),
- ("/path/param-lt0/-1", 200, -1),
- ("/path/param-lt0/0", 422, response_less_than_0),
- ("/path/param-le/42", 422, response_less_than_equal_3),
- ("/path/param-le/3", 200, 3),
- ("/path/param-le/2", 200, 2),
- ("/path/param-lt-gt/2", 200, 2),
- ("/path/param-lt-gt/4", 422, response_less_than_3),
- ("/path/param-lt-gt/0", 422, response_greater_than_1),
- ("/path/param-le-ge/2", 200, 2),
- ("/path/param-le-ge/1", 200, 1),
- ("/path/param-le-ge/3", 200, 3),
- ("/path/param-le-ge/4", 422, response_less_than_equal_3),
- ("/path/param-lt-int/2", 200, 2),
- ("/path/param-lt-int/42", 422, response_less_than_3),
- ("/path/param-lt-int/2.7", 422, response_not_valid_int),
- ("/path/param-gt-int/42", 200, 42),
- ("/path/param-gt-int/2", 422, response_greater_than_3),
- ("/path/param-gt-int/2.7", 422, response_not_valid_int),
- ("/path/param-le-int/42", 422, response_less_than_equal_3),
- ("/path/param-le-int/3", 200, 3),
- ("/path/param-le-int/2", 200, 2),
- ("/path/param-le-int/2.7", 422, response_not_valid_int),
- ("/path/param-ge-int/42", 200, 42),
- ("/path/param-ge-int/3", 200, 3),
- ("/path/param-ge-int/2", 422, response_greater_than_equal_3),
- ("/path/param-ge-int/2.7", 422, response_not_valid_int),
- ("/path/param-lt-gt-int/2", 200, 2),
- ("/path/param-lt-gt-int/4", 422, response_less_than_3),
- ("/path/param-lt-gt-int/0", 422, response_greater_than_1),
- ("/path/param-lt-gt-int/2.7", 422, response_not_valid_int),
- ("/path/param-le-ge-int/2", 200, 2),
- ("/path/param-le-ge-int/1", 200, 1),
- ("/path/param-le-ge-int/3", 200, 3),
- ("/path/param-le-ge-int/4", 422, response_less_than_equal_3),
- ("/path/param-le-ge-int/2.7", 422, response_not_valid_int),
- ],
-)
-def test_get_path(path, expected_status, expected_response):
- response = client.get(path)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+ )
-import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from .main import app
client = TestClient(app)
-response_missing = {
- "detail": [
- {
- "loc": ["query", "query"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
-}
-
-response_not_valid_int = {
- "detail": [
- {
- "loc": ["query", "query"],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
- }
- ]
-}
-
-
-@pytest.mark.parametrize(
- "path,expected_status,expected_response",
- [
- ("/query", 422, response_missing),
- ("/query?query=baz", 200, "foo bar baz"),
- ("/query?not_declared=baz", 422, response_missing),
- ("/query/optional", 200, "foo bar"),
- ("/query/optional?query=baz", 200, "foo bar baz"),
- ("/query/optional?not_declared=baz", 200, "foo bar"),
- ("/query/int", 422, response_missing),
- ("/query/int?query=42", 200, "foo bar 42"),
- ("/query/int?query=42.5", 422, response_not_valid_int),
- ("/query/int?query=baz", 422, response_not_valid_int),
- ("/query/int?not_declared=baz", 422, response_missing),
- ("/query/int/optional", 200, "foo bar"),
- ("/query/int/optional?query=50", 200, "foo bar 50"),
- ("/query/int/optional?query=foo", 422, response_not_valid_int),
- ("/query/int/default", 200, "foo bar 10"),
- ("/query/int/default?query=50", 200, "foo bar 50"),
- ("/query/int/default?query=foo", 422, response_not_valid_int),
- ("/query/param", 200, "foo bar"),
- ("/query/param?query=50", 200, "foo bar 50"),
- ("/query/param-required", 422, response_missing),
- ("/query/param-required?query=50", 200, "foo bar 50"),
- ("/query/param-required/int", 422, response_missing),
- ("/query/param-required/int?query=50", 200, "foo bar 50"),
- ("/query/param-required/int?query=foo", 422, response_not_valid_int),
- ("/query/frozenset/?query=1&query=1&query=2", 200, "1,2"),
- ],
-)
-def test_get_path(path, expected_status, expected_response):
- response = client.get(path)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+
+def test_query():
+ response = client.get("/query")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "query"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "query"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_query_query_baz():
+ response = client.get("/query?query=baz")
+ assert response.status_code == 200
+ assert response.json() == "foo bar baz"
+
+
+def test_query_not_declared_baz():
+ response = client.get("/query?not_declared=baz")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "query"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "query"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_query_optional():
+ response = client.get("/query/optional")
+ assert response.status_code == 200
+ assert response.json() == "foo bar"
+
+
+def test_query_optional_query_baz():
+ response = client.get("/query/optional?query=baz")
+ assert response.status_code == 200
+ assert response.json() == "foo bar baz"
+
+
+def test_query_optional_not_declared_baz():
+ response = client.get("/query/optional?not_declared=baz")
+ assert response.status_code == 200
+ assert response.json() == "foo bar"
+
+
+def test_query_int():
+ response = client.get("/query/int")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "query"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "query"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_query_int_query_42():
+ response = client.get("/query/int?query=42")
+ assert response.status_code == 200
+ assert response.json() == "foo bar 42"
+
+
+def test_query_int_query_42_5():
+ response = client.get("/query/int?query=42.5")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["query", "query"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "42.5",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "query"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
+
+
+def test_query_int_query_baz():
+ response = client.get("/query/int?query=baz")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["query", "query"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "baz",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "query"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
+
+
+def test_query_int_not_declared_baz():
+ response = client.get("/query/int?not_declared=baz")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "query"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "query"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_query_int_optional():
+ response = client.get("/query/int/optional")
+ assert response.status_code == 200
+ assert response.json() == "foo bar"
+
+
+def test_query_int_optional_query_50():
+ response = client.get("/query/int/optional?query=50")
+ assert response.status_code == 200
+ assert response.json() == "foo bar 50"
+
+
+def test_query_int_optional_query_foo():
+ response = client.get("/query/int/optional?query=foo")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["query", "query"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "foo",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "query"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
+
+
+def test_query_int_default():
+ response = client.get("/query/int/default")
+ assert response.status_code == 200
+ assert response.json() == "foo bar 10"
+
+
+def test_query_int_default_query_50():
+ response = client.get("/query/int/default?query=50")
+ assert response.status_code == 200
+ assert response.json() == "foo bar 50"
+
+
+def test_query_int_default_query_foo():
+ response = client.get("/query/int/default?query=foo")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["query", "query"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "foo",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "query"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
+
+
+def test_query_param():
+ response = client.get("/query/param")
+ assert response.status_code == 200
+ assert response.json() == "foo bar"
+
+
+def test_query_param_query_50():
+ response = client.get("/query/param?query=50")
+ assert response.status_code == 200
+ assert response.json() == "foo bar 50"
+
+
+def test_query_param_required():
+ response = client.get("/query/param-required")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "query"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "query"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_query_param_required_query_50():
+ response = client.get("/query/param-required?query=50")
+ assert response.status_code == 200
+ assert response.json() == "foo bar 50"
+
+
+def test_query_param_required_int():
+ response = client.get("/query/param-required/int")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "query"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "query"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_query_param_required_int_query_50():
+ response = client.get("/query/param-required/int?query=50")
+ assert response.status_code == 200
+ assert response.json() == "foo bar 50"
+
+
+def test_query_param_required_int_query_foo():
+ response = client.get("/query/param-required/int?query=foo")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["query", "query"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "foo",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "query"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
+
+
+def test_query_frozenset_query_1_query_1_query_2():
+ response = client.get("/query/frozenset/?query=1&query=1&query=2")
+ assert response.status_code == 200
+ assert response.json() == "1,2"
from fastapi import FastAPI
from fastapi.testclient import TestClient
-from pydantic import BaseModel
+from pydantic import BaseModel, ConfigDict
+from .utils import needs_pydanticv1, needs_pydanticv2
-class PersonBase(BaseModel):
- name: str
- lastname: str
+@needs_pydanticv2
+def test_read_with_orm_mode() -> None:
+ class PersonBase(BaseModel):
+ name: str
+ lastname: str
+
+ class Person(PersonBase):
+ @property
+ def full_name(self) -> str:
+ return f"{self.name} {self.lastname}"
+
+ model_config = ConfigDict(from_attributes=True)
+
+ class PersonCreate(PersonBase):
+ pass
-class Person(PersonBase):
- @property
- def full_name(self) -> str:
- return f"{self.name} {self.lastname}"
+ class PersonRead(PersonBase):
+ full_name: str
- class Config:
- orm_mode = True
- read_with_orm_mode = True
+ model_config = {"from_attributes": True}
+ app = FastAPI()
-class PersonCreate(PersonBase):
- pass
+ @app.post("/people/", response_model=PersonRead)
+ def create_person(person: PersonCreate) -> Any:
+ db_person = Person.model_validate(person)
+ return db_person
+
+ client = TestClient(app)
+
+ person_data = {"name": "Dive", "lastname": "Wilson"}
+ response = client.post("/people/", json=person_data)
+ data = response.json()
+ assert response.status_code == 200, response.text
+ assert data["name"] == person_data["name"]
+ assert data["lastname"] == person_data["lastname"]
+ assert data["full_name"] == person_data["name"] + " " + person_data["lastname"]
-class PersonRead(PersonBase):
- full_name: str
+@needs_pydanticv1
+def test_read_with_orm_mode_pv1() -> None:
+ class PersonBase(BaseModel):
+ name: str
+ lastname: str
- class Config:
- orm_mode = True
+ class Person(PersonBase):
+ @property
+ def full_name(self) -> str:
+ return f"{self.name} {self.lastname}"
+ class Config:
+ orm_mode = True
+ read_with_orm_mode = True
-app = FastAPI()
+ class PersonCreate(PersonBase):
+ pass
+ class PersonRead(PersonBase):
+ full_name: str
-@app.post("/people/", response_model=PersonRead)
-def create_person(person: PersonCreate) -> Any:
- db_person = Person.from_orm(person)
- return db_person
+ class Config:
+ orm_mode = True
+ app = FastAPI()
-client = TestClient(app)
+ @app.post("/people/", response_model=PersonRead)
+ def create_person(person: PersonCreate) -> Any:
+ db_person = Person.from_orm(person)
+ return db_person
+ client = TestClient(app)
-def test_read_with_orm_mode() -> None:
person_data = {"name": "Dive", "lastname": "Wilson"}
response = client.post("/people/", json=person_data)
data = response.json()
--- /dev/null
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, Form
+from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
+from typing_extensions import Annotated
+
+from .utils import needs_py310
+
+
+def get_client():
+ app = FastAPI()
+ with pytest.warns(DeprecationWarning):
+
+ @app.post("/items/")
+ async def read_items(
+ q: Annotated[str | None, Form(regex="^fixedquery$")] = None
+ ):
+ if q:
+ return f"Hello {q}"
+ else:
+ return "Hello World"
+
+ client = TestClient(app)
+ return client
+
+
+@needs_py310
+def test_no_query():
+ client = get_client()
+ response = client.post("/items/")
+ assert response.status_code == 200
+ assert response.json() == "Hello World"
+
+
+@needs_py310
+def test_q_fixedquery():
+ client = get_client()
+ response = client.post("/items/", data={"q": "fixedquery"})
+ assert response.status_code == 200
+ assert response.json() == "Hello fixedquery"
+
+
+@needs_py310
+def test_query_nonregexquery():
+ client = get_client()
+ response = client.post("/items/", data={"q": "nonregexquery"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "string_pattern_mismatch",
+ "loc": ["body", "q"],
+ "msg": "String should match pattern '^fixedquery$'",
+ "input": "nonregexquery",
+ "ctx": {"pattern": "^fixedquery$"},
+ "url": match_pydantic_error_url("string_pattern_mismatch"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "ctx": {"pattern": "^fixedquery$"},
+ "loc": ["body", "q"],
+ "msg": 'string does not match regex "^fixedquery$"',
+ "type": "value_error.str.regex",
+ }
+ ]
+ }
+ )
+
+
+@needs_py310
+def test_openapi_schema():
+ client = get_client()
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ # insert_assert(response.json())
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/items/": {
+ "post": {
+ "summary": "Read Items",
+ "operationId": "read_items_items__post",
+ "requestBody": {
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": IsDict(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Body_read_items_items__post"
+ }
+ ],
+ "title": "Body",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "$ref": "#/components/schemas/Body_read_items_items__post"
+ }
+ )
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "Body_read_items_items__post": {
+ "properties": {
+ "q": IsDict(
+ {
+ "anyOf": [
+ {"type": "string", "pattern": "^fixedquery$"},
+ {"type": "null"},
+ ],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"type": "string", "pattern": "^fixedquery$", "title": "Q"}
+ )
+ },
+ "type": "object",
+ "title": "Body_read_items_items__post",
+ },
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }
--- /dev/null
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, Query
+from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
+from typing_extensions import Annotated
+
+from .utils import needs_py310
+
+
+def get_client():
+ app = FastAPI()
+ with pytest.warns(DeprecationWarning):
+
+ @app.get("/items/")
+ async def read_items(
+ q: Annotated[str | None, Query(regex="^fixedquery$")] = None
+ ):
+ if q:
+ return f"Hello {q}"
+ else:
+ return "Hello World"
+
+ client = TestClient(app)
+ return client
+
+
+@needs_py310
+def test_query_params_str_validations_no_query():
+ client = get_client()
+ response = client.get("/items/")
+ assert response.status_code == 200
+ assert response.json() == "Hello World"
+
+
+@needs_py310
+def test_query_params_str_validations_q_fixedquery():
+ client = get_client()
+ response = client.get("/items/", params={"q": "fixedquery"})
+ assert response.status_code == 200
+ assert response.json() == "Hello fixedquery"
+
+
+@needs_py310
+def test_query_params_str_validations_item_query_nonregexquery():
+ client = get_client()
+ response = client.get("/items/", params={"q": "nonregexquery"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "string_pattern_mismatch",
+ "loc": ["query", "q"],
+ "msg": "String should match pattern '^fixedquery$'",
+ "input": "nonregexquery",
+ "ctx": {"pattern": "^fixedquery$"},
+ "url": match_pydantic_error_url("string_pattern_mismatch"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "ctx": {"pattern": "^fixedquery$"},
+ "loc": ["query", "q"],
+ "msg": 'string does not match regex "^fixedquery$"',
+ "type": "value_error.str.regex",
+ }
+ ]
+ }
+ )
+
+
+@needs_py310
+def test_openapi_schema():
+ client = get_client()
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ # insert_assert(response.json())
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/items/": {
+ "get": {
+ "summary": "Read Items",
+ "operationId": "read_items_items__get",
+ "parameters": [
+ {
+ "name": "q",
+ "in": "query",
+ "required": False,
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {"type": "string", "pattern": "^fixedquery$"},
+ {"type": "null"},
+ ],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "type": "string",
+ "pattern": "^fixedquery$",
+ "title": "Q",
+ }
+ ),
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
- # insert_assert(response.json())
assert response.json() == {
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
from typing import List
from fastapi import FastAPI
+from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, ConfigDict, Field
app = FastAPI()
class ModelNoAlias(BaseModel):
name: str
- class Config:
- schema_extra = {
- "description": (
- "response_model_by_alias=False is basically a quick hack, to support "
- "proper OpenAPI use another model with the correct field names"
- )
- }
+ if PYDANTIC_V2:
+ model_config = ConfigDict(
+ json_schema_extra={
+ "description": (
+ "response_model_by_alias=False is basically a quick hack, to support "
+ "proper OpenAPI use another model with the correct field names"
+ )
+ }
+ )
+ else:
+
+ class Config:
+ schema_extra = {
+ "description": (
+ "response_model_by_alias=False is basically a quick hack, to support "
+ "proper OpenAPI use another model with the correct field names"
+ )
+ }
@app.get("/dict", response_model=Model, response_model_by_alias=False)
import pytest
from fastapi import FastAPI
-from fastapi.exceptions import FastAPIError
+from fastapi.exceptions import FastAPIError, ResponseValidationError
from fastapi.responses import JSONResponse, Response
from fastapi.testclient import TestClient
-from pydantic import BaseModel, ValidationError
+from pydantic import BaseModel
class BaseUser(BaseModel):
def test_response_model_no_annotation_return_invalid_dict():
- with pytest.raises(ValidationError):
+ with pytest.raises(ResponseValidationError):
client.get("/response_model-no_annotation-return_invalid_dict")
def test_response_model_no_annotation_return_invalid_model():
- with pytest.raises(ValidationError):
+ with pytest.raises(ResponseValidationError):
client.get("/response_model-no_annotation-return_invalid_model")
def test_no_response_model_annotation_return_invalid_dict():
- with pytest.raises(ValidationError):
+ with pytest.raises(ResponseValidationError):
client.get("/no_response_model-annotation-return_invalid_dict")
def test_no_response_model_annotation_return_invalid_model():
- with pytest.raises(ValidationError):
+ with pytest.raises(ResponseValidationError):
client.get("/no_response_model-annotation-return_invalid_model")
def test_response_model_model1_annotation_model2_return_invalid_dict():
- with pytest.raises(ValidationError):
+ with pytest.raises(ResponseValidationError):
client.get("/response_model_model1-annotation_model2-return_invalid_dict")
def test_response_model_model1_annotation_model2_return_invalid_model():
- with pytest.raises(ValidationError):
+ with pytest.raises(ResponseValidationError):
client.get("/response_model_model1-annotation_model2-return_invalid_model")
--- /dev/null
+from typing import List
+
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class UserBase(BaseModel):
+ email: str
+
+
+class UserCreate(UserBase):
+ password: str
+
+
+class UserDB(UserBase):
+ hashed_password: str
+
+
+class PetDB(BaseModel):
+ name: str
+ owner: UserDB
+
+
+class PetOut(BaseModel):
+ name: str
+ owner: UserBase
+
+
+@app.post("/users/", response_model=UserBase)
+async def create_user(user: UserCreate):
+ return user
+
+
+@app.get("/pets/{pet_id}", response_model=PetOut)
+async def read_pet(pet_id: int):
+ user = UserDB(
+ email="johndoe@example.com",
+ hashed_password="secrethashed",
+ )
+ pet = PetDB(name="Nibbler", owner=user)
+ return pet
+
+
+@app.get("/pets/", response_model=List[PetOut])
+async def read_pets():
+ user = UserDB(
+ email="johndoe@example.com",
+ hashed_password="secrethashed",
+ )
+ pet1 = PetDB(name="Nibbler", owner=user)
+ pet2 = PetDB(name="Zoidberg", owner=user)
+ return [pet1, pet2]
+
+
+client = TestClient(app)
+
+
+def test_filter_top_level_model():
+ response = client.post(
+ "/users", json={"email": "johndoe@example.com", "password": "secret"}
+ )
+ assert response.json() == {"email": "johndoe@example.com"}
+
+
+def test_filter_second_level_model():
+ response = client.get("/pets/1")
+ assert response.json() == {
+ "name": "Nibbler",
+ "owner": {"email": "johndoe@example.com"},
+ }
+
+
+def test_list_of_models():
+ response = client.get("/pets/")
+ assert response.json() == [
+ {"name": "Nibbler", "owner": {"email": "johndoe@example.com"}},
+ {"name": "Zoidberg", "owner": {"email": "johndoe@example.com"}},
+ ]
--- /dev/null
+from typing import List
+
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class UserCreate(BaseModel):
+ email: str
+ password: str
+
+
+class UserDB(BaseModel):
+ email: str
+ hashed_password: str
+
+
+class User(BaseModel):
+ email: str
+
+
+class PetDB(BaseModel):
+ name: str
+ owner: UserDB
+
+
+class PetOut(BaseModel):
+ name: str
+ owner: User
+
+
+@app.post("/users/", response_model=User)
+async def create_user(user: UserCreate):
+ return user
+
+
+@app.get("/pets/{pet_id}", response_model=PetOut)
+async def read_pet(pet_id: int):
+ user = UserDB(
+ email="johndoe@example.com",
+ hashed_password="secrethashed",
+ )
+ pet = PetDB(name="Nibbler", owner=user)
+ return pet
+
+
+@app.get("/pets/", response_model=List[PetOut])
+async def read_pets():
+ user = UserDB(
+ email="johndoe@example.com",
+ hashed_password="secrethashed",
+ )
+ pet1 = PetDB(name="Nibbler", owner=user)
+ pet2 = PetDB(name="Zoidberg", owner=user)
+ return [pet1, pet2]
+
+
+client = TestClient(app)
+
+
+def test_filter_top_level_model():
+ response = client.post(
+ "/users", json={"email": "johndoe@example.com", "password": "secret"}
+ )
+ assert response.json() == {"email": "johndoe@example.com"}
+
+
+def test_filter_second_level_model():
+ response = client.get("/pets/1")
+ assert response.json() == {
+ "name": "Nibbler",
+ "owner": {"email": "johndoe@example.com"},
+ }
+
+
+def test_list_of_models():
+ response = client.get("/pets/")
+ assert response.json() == [
+ {"name": "Nibbler", "owner": {"email": "johndoe@example.com"}},
+ {"name": "Zoidberg", "owner": {"email": "johndoe@example.com"}},
+ ]
from typing import Union
import pytest
+from dirty_equals import IsDict
from fastapi import Body, Cookie, FastAPI, Header, Path, Query
+from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
-from pydantic import BaseModel
+from pydantic import BaseModel, ConfigDict
def create_app():
class Item(BaseModel):
data: str
- class Config:
- schema_extra = {"example": {"data": "Data in schema_extra"}}
+ if PYDANTIC_V2:
+ model_config = ConfigDict(
+ json_schema_extra={"example": {"data": "Data in schema_extra"}}
+ )
+ else:
+
+ class Config:
+ schema_extra = {"example": {"data": "Data in schema_extra"}}
@app.post("/schema_extra/")
def schema_extra(item: Item):
"requestBody": {
"content": {
"application/json": {
- "schema": {
- "allOf": [{"$ref": "#/components/schemas/Item"}],
- "title": "Item",
- "examples": [
- {"data": "Data in Body examples, example1"},
- {"data": "Data in Body examples, example2"},
- ],
- }
+ "schema": IsDict(
+ {
+ "$ref": "#/components/schemas/Item",
+ "examples": [
+ {"data": "Data in Body examples, example1"},
+ {"data": "Data in Body examples, example2"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove this when deprecating Pydantic v1
+ {
+ "allOf": [
+ {"$ref": "#/components/schemas/Item"}
+ ],
+ "title": "Item",
+ "examples": [
+ {"data": "Data in Body examples, example1"},
+ {"data": "Data in Body examples, example2"},
+ ],
+ }
+ )
}
},
"required": True,
"requestBody": {
"content": {
"application/json": {
- "schema": {
- "allOf": [{"$ref": "#/components/schemas/Item"}],
- "title": "Item",
- "examples": [
- {"data": "examples example_examples 1"},
- {"data": "examples example_examples 2"},
- ],
- },
+ "schema": IsDict(
+ {
+ "$ref": "#/components/schemas/Item",
+ "examples": [
+ {"data": "examples example_examples 1"},
+ {"data": "examples example_examples 2"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove this when deprecating Pydantic v1
+ {
+ "allOf": [
+ {"$ref": "#/components/schemas/Item"}
+ ],
+ "title": "Item",
+ "examples": [
+ {"data": "examples example_examples 1"},
+ {"data": "examples example_examples 2"},
+ ],
+ },
+ ),
"example": {"data": "Overridden example"},
}
},
"parameters": [
{
"required": False,
- "schema": {"title": "Data", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Data",
+ }
+ )
+ | IsDict(
+ # TODO: Remove this when deprecating Pydantic v1
+ {"title": "Data", "type": "string"}
+ ),
"example": "query1",
"name": "data",
"in": "query",
"parameters": [
{
"required": False,
- "schema": {
- "type": "string",
- "title": "Data",
- "examples": ["query1", "query2"],
- },
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Data",
+ "examples": ["query1", "query2"],
+ }
+ )
+ | IsDict(
+ # TODO: Remove this when deprecating Pydantic v1
+ {
+ "type": "string",
+ "title": "Data",
+ "examples": ["query1", "query2"],
+ }
+ ),
"name": "data",
"in": "query",
}
"parameters": [
{
"required": False,
- "schema": {
- "type": "string",
- "title": "Data",
- "examples": ["query1", "query2"],
- },
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Data",
+ "examples": ["query1", "query2"],
+ }
+ )
+ | IsDict(
+ # TODO: Remove this when deprecating Pydantic v1
+ {
+ "type": "string",
+ "title": "Data",
+ "examples": ["query1", "query2"],
+ }
+ ),
"example": "query_overridden",
"name": "data",
"in": "query",
"parameters": [
{
"required": False,
- "schema": {"type": "string", "title": "Data"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Data",
+ }
+ )
+ | IsDict(
+ # TODO: Remove this when deprecating Pydantic v1
+ {"title": "Data", "type": "string"}
+ ),
"example": "header1",
"name": "data",
"in": "header",
"parameters": [
{
"required": False,
- "schema": {
- "type": "string",
- "title": "Data",
- "examples": ["header1", "header2"],
- },
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Data",
+ "examples": ["header1", "header2"],
+ }
+ )
+ | IsDict(
+ # TODO: Remove this when deprecating Pydantic v1
+ {
+ "type": "string",
+ "title": "Data",
+ "examples": ["header1", "header2"],
+ }
+ ),
"name": "data",
"in": "header",
}
"parameters": [
{
"required": False,
- "schema": {
- "type": "string",
- "title": "Data",
- "examples": ["header1", "header2"],
- },
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Data",
+ "examples": ["header1", "header2"],
+ }
+ )
+ | IsDict(
+ # TODO: Remove this when deprecating Pydantic v1
+ {
+ "title": "Data",
+ "type": "string",
+ "examples": ["header1", "header2"],
+ }
+ ),
"example": "header_overridden",
"name": "data",
"in": "header",
"parameters": [
{
"required": False,
- "schema": {"type": "string", "title": "Data"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Data",
+ }
+ )
+ | IsDict(
+ # TODO: Remove this when deprecating Pydantic v1
+ {"title": "Data", "type": "string"}
+ ),
"example": "cookie1",
"name": "data",
"in": "cookie",
"parameters": [
{
"required": False,
- "schema": {
- "type": "string",
- "title": "Data",
- "examples": ["cookie1", "cookie2"],
- },
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Data",
+ "examples": ["cookie1", "cookie2"],
+ }
+ )
+ | IsDict(
+ # TODO: Remove this when deprecating Pydantic v1
+ {
+ "title": "Data",
+ "type": "string",
+ "examples": ["cookie1", "cookie2"],
+ }
+ ),
"name": "data",
"in": "cookie",
}
"parameters": [
{
"required": False,
- "schema": {
- "type": "string",
- "title": "Data",
- "examples": ["cookie1", "cookie2"],
- },
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Data",
+ "examples": ["cookie1", "cookie2"],
+ }
+ )
+ | IsDict(
+ # TODO: Remove this when deprecating Pydantic v1
+ {
+ "title": "Data",
+ "type": "string",
+ "examples": ["cookie1", "cookie2"],
+ }
+ ),
"example": "cookie_overridden",
"name": "data",
"in": "cookie",
-import pytest
+from dirty_equals import IsDict
from fastapi import Depends, FastAPI, Security
from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel
app = FastAPI()
assert response.json() == {"detail": "Not authenticated"}
-required_params = {
- "detail": [
+def test_strict_login_no_data():
+ response = client.post("/login")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "grant_type"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "username"],
- "msg": "field required",
- "type": "value_error.missing",
- },
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "grant_type"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["body", "password"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
+ "detail": [
+ {
+ "loc": ["body", "grant_type"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-grant_type_required = {
- "detail": [
+
+def test_strict_login_no_grant_type():
+ response = client.post("/login", data={"username": "johndoe", "password": "secret"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "grant_type"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["body", "grant_type"],
- "msg": "field required",
- "type": "value_error.missing",
+ "detail": [
+ {
+ "loc": ["body", "grant_type"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
}
- ]
-}
+ )
-grant_type_incorrect = {
- "detail": [
+
+def test_strict_login_incorrect_grant_type():
+ response = client.post(
+ "/login",
+ data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
+ )
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "grant_type"],
- "msg": 'string does not match regex "password"',
- "type": "value_error.str.regex",
- "ctx": {"pattern": "password"},
+ "detail": [
+ {
+ "type": "string_pattern_mismatch",
+ "loc": ["body", "grant_type"],
+ "msg": "String should match pattern 'password'",
+ "input": "incorrect",
+ "ctx": {"pattern": "password"},
+ "url": match_pydantic_error_url("string_pattern_mismatch"),
+ }
+ ]
}
- ]
-}
-
-
-@pytest.mark.parametrize(
- "data,expected_status,expected_response",
- [
- (None, 422, required_params),
- ({"username": "johndoe", "password": "secret"}, 422, grant_type_required),
- (
- {"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
- 422,
- grant_type_incorrect,
- ),
- (
- {"username": "johndoe", "password": "secret", "grant_type": "password"},
- 200,
- {
- "grant_type": "password",
- "username": "johndoe",
- "password": "secret",
- "scopes": [],
- "client_id": None,
- "client_secret": None,
- },
- ),
- ],
-)
-def test_strict_login(data, expected_status, expected_response):
- response = client.post("/login", data=data)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "grant_type"],
+ "msg": 'string does not match regex "password"',
+ "type": "value_error.str.regex",
+ "ctx": {"pattern": "password"},
+ }
+ ]
+ }
+ )
+
+
+def test_strict_login_correct_grant_type():
+ response = client.post(
+ "/login",
+ data={"username": "johndoe", "password": "secret", "grant_type": "password"},
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "grant_type": "password",
+ "username": "johndoe",
+ "password": "secret",
+ "scopes": [],
+ "client_id": None,
+ "client_secret": None,
+ }
def test_openapi_schema():
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
- "client_id": {"title": "Client Id", "type": "string"},
- "client_secret": {"title": "Client Secret", "type": "string"},
+ "client_id": IsDict(
+ {
+ "title": "Client Id",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Id", "type": "string"}
+ ),
+ "client_secret": IsDict(
+ {
+ "title": "Client Secret",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Secret", "type": "string"}
+ ),
},
},
"ValidationError": {
from typing import Optional
-import pytest
+from dirty_equals import IsDict
from fastapi import Depends, FastAPI, Security
from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel
app = FastAPI()
assert response.json() == {"msg": "Create an account first"}
-required_params = {
- "detail": [
+def test_strict_login_no_data():
+ response = client.post("/login")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "grant_type"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "username"],
- "msg": "field required",
- "type": "value_error.missing",
- },
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "grant_type"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["body", "password"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
+ "detail": [
+ {
+ "loc": ["body", "grant_type"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-grant_type_required = {
- "detail": [
+
+def test_strict_login_no_grant_type():
+ response = client.post("/login", data={"username": "johndoe", "password": "secret"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "grant_type"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["body", "grant_type"],
- "msg": "field required",
- "type": "value_error.missing",
+ "detail": [
+ {
+ "loc": ["body", "grant_type"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
}
- ]
-}
+ )
-grant_type_incorrect = {
- "detail": [
+
+def test_strict_login_incorrect_grant_type():
+ response = client.post(
+ "/login",
+ data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
+ )
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "grant_type"],
- "msg": 'string does not match regex "password"',
- "type": "value_error.str.regex",
- "ctx": {"pattern": "password"},
+ "detail": [
+ {
+ "type": "string_pattern_mismatch",
+ "loc": ["body", "grant_type"],
+ "msg": "String should match pattern 'password'",
+ "input": "incorrect",
+ "ctx": {"pattern": "password"},
+ "url": match_pydantic_error_url("string_pattern_mismatch"),
+ }
+ ]
}
- ]
-}
-
-
-@pytest.mark.parametrize(
- "data,expected_status,expected_response",
- [
- (None, 422, required_params),
- ({"username": "johndoe", "password": "secret"}, 422, grant_type_required),
- (
- {"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
- 422,
- grant_type_incorrect,
- ),
- (
- {"username": "johndoe", "password": "secret", "grant_type": "password"},
- 200,
- {
- "grant_type": "password",
- "username": "johndoe",
- "password": "secret",
- "scopes": [],
- "client_id": None,
- "client_secret": None,
- },
- ),
- ],
-)
-def test_strict_login(data, expected_status, expected_response):
- response = client.post("/login", data=data)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "grant_type"],
+ "msg": 'string does not match regex "password"',
+ "type": "value_error.str.regex",
+ "ctx": {"pattern": "password"},
+ }
+ ]
+ }
+ )
+
+
+def test_strict_login_correct_data():
+ response = client.post(
+ "/login",
+ data={"username": "johndoe", "password": "secret", "grant_type": "password"},
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "grant_type": "password",
+ "username": "johndoe",
+ "password": "secret",
+ "scopes": [],
+ "client_id": None,
+ "client_secret": None,
+ }
def test_openapi_schema():
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
- "client_id": {"title": "Client Id", "type": "string"},
- "client_secret": {"title": "Client Secret", "type": "string"},
+ "client_id": IsDict(
+ {
+ "title": "Client Id",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Id", "type": "string"}
+ ),
+ "client_secret": IsDict(
+ {
+ "title": "Client Secret",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Secret", "type": "string"}
+ ),
},
},
"ValidationError": {
from typing import Optional
-import pytest
+from dirty_equals import IsDict
from fastapi import Depends, FastAPI, Security
from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from pydantic import BaseModel
app = FastAPI()
assert response.json() == {"msg": "Create an account first"}
-required_params = {
- "detail": [
+def test_strict_login_None():
+ response = client.post("/login", data=None)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "grant_type"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "username"],
- "msg": "field required",
- "type": "value_error.missing",
- },
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "grant_type"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["body", "password"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
+ "detail": [
+ {
+ "loc": ["body", "grant_type"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-grant_type_required = {
- "detail": [
+
+def test_strict_login_no_grant_type():
+ response = client.post("/login", data={"username": "johndoe", "password": "secret"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "grant_type"],
- "msg": "field required",
- "type": "value_error.missing",
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "grant_type"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
}
- ]
-}
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "grant_type"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
-grant_type_incorrect = {
- "detail": [
+def test_strict_login_incorrect_grant_type():
+ response = client.post(
+ "/login",
+ data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
+ )
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "grant_type"],
- "msg": 'string does not match regex "password"',
- "type": "value_error.str.regex",
- "ctx": {"pattern": "password"},
+ "detail": [
+ {
+ "type": "string_pattern_mismatch",
+ "loc": ["body", "grant_type"],
+ "msg": "String should match pattern 'password'",
+ "input": "incorrect",
+ "ctx": {"pattern": "password"},
+ "url": match_pydantic_error_url("string_pattern_mismatch"),
+ }
+ ]
}
- ]
-}
-
-
-@pytest.mark.parametrize(
- "data,expected_status,expected_response",
- [
- (None, 422, required_params),
- ({"username": "johndoe", "password": "secret"}, 422, grant_type_required),
- (
- {"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
- 422,
- grant_type_incorrect,
- ),
- (
- {"username": "johndoe", "password": "secret", "grant_type": "password"},
- 200,
- {
- "grant_type": "password",
- "username": "johndoe",
- "password": "secret",
- "scopes": [],
- "client_id": None,
- "client_secret": None,
- },
- ),
- ],
-)
-def test_strict_login(data, expected_status, expected_response):
- response = client.post("/login", data=data)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "grant_type"],
+ "msg": 'string does not match regex "password"',
+ "type": "value_error.str.regex",
+ "ctx": {"pattern": "password"},
+ }
+ ]
+ }
+ )
+
+
+def test_strict_login_correct_correct_grant_type():
+ response = client.post(
+ "/login",
+ data={"username": "johndoe", "password": "secret", "grant_type": "password"},
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "grant_type": "password",
+ "username": "johndoe",
+ "password": "secret",
+ "scopes": [],
+ "client_id": None,
+ "client_secret": None,
+ }
def test_openapi_schema():
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
- "client_id": {"title": "Client Id", "type": "string"},
- "client_secret": {"title": "Client Secret", "type": "string"},
+ "client_id": IsDict(
+ {
+ "title": "Client Id",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Id", "type": "string"}
+ ),
+ "client_secret": IsDict(
+ {
+ "title": "Client Secret",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Secret", "type": "string"}
+ ),
},
},
"ValidationError": {
class Model(BaseModel):
- x: Optional[int]
+ x: Optional[int] = None
sub: SubModel
from typing import Optional
+from dirty_equals import IsDict
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, HttpUrl
"parameters": [
{
"required": False,
- "schema": {
- "title": "Callback Url",
- "maxLength": 2083,
- "minLength": 1,
- "type": "string",
- "format": "uri",
- },
+ "schema": IsDict(
+ {
+ "title": "Callback Url",
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "uri",
+ "minLength": 1,
+ "maxLength": 2083,
+ },
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Callback Url",
+ "maxLength": 2083,
+ "minLength": 1,
+ "type": "string",
+ "format": "uri",
+ }
+ ),
"name": "callback_url",
"in": "query",
}
"type": "object",
"properties": {
"id": {"title": "Id", "type": "string"},
- "title": {"title": "Title", "type": "string"},
+ "title": IsDict(
+ {
+ "title": "Title",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Title", "type": "string"}
+ ),
"customer": {"title": "Customer", "type": "string"},
"total": {"title": "Total", "type": "number"},
},
from typing import List, Tuple
+from dirty_equals import IsDict
from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from pydantic import BaseModel
"requestBody": {
"content": {
"application/json": {
- "schema": {
- "title": "Square",
- "maxItems": 2,
- "minItems": 2,
- "type": "array",
- "items": [
- {"$ref": "#/components/schemas/Coordinate"},
- {"$ref": "#/components/schemas/Coordinate"},
- ],
- }
+ "schema": IsDict(
+ {
+ "title": "Square",
+ "maxItems": 2,
+ "minItems": 2,
+ "type": "array",
+ "prefixItems": [
+ {"$ref": "#/components/schemas/Coordinate"},
+ {"$ref": "#/components/schemas/Coordinate"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Square",
+ "maxItems": 2,
+ "minItems": 2,
+ "type": "array",
+ "items": [
+ {"$ref": "#/components/schemas/Coordinate"},
+ {"$ref": "#/components/schemas/Coordinate"},
+ ],
+ }
+ )
}
},
"required": True,
"required": ["values"],
"type": "object",
"properties": {
- "values": {
- "title": "Values",
- "maxItems": 2,
- "minItems": 2,
- "type": "array",
- "items": [{"type": "integer"}, {"type": "integer"}],
- }
+ "values": IsDict(
+ {
+ "title": "Values",
+ "maxItems": 2,
+ "minItems": 2,
+ "type": "array",
+ "prefixItems": [
+ {"type": "integer"},
+ {"type": "integer"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Values",
+ "maxItems": 2,
+ "minItems": 2,
+ "type": "array",
+ "items": [{"type": "integer"}, {"type": "integer"}],
+ }
+ )
},
},
"Coordinate": {
"items": {
"title": "Items",
"type": "array",
- "items": {
- "maxItems": 2,
- "minItems": 2,
- "type": "array",
- "items": [{"type": "string"}, {"type": "string"}],
- },
+ "items": IsDict(
+ {
+ "maxItems": 2,
+ "minItems": 2,
+ "type": "array",
+ "prefixItems": [
+ {"type": "string"},
+ {"type": "string"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "maxItems": 2,
+ "minItems": 2,
+ "type": "array",
+ "items": [{"type": "string"}, {"type": "string"}],
+ }
+ ),
}
},
},
import os
import shutil
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.additional_responses.tutorial002 import app
},
{
"required": False,
- "schema": {"title": "Img", "type": "boolean"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "boolean"}, {"type": "null"}],
+ "title": "Img",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Img", "type": "boolean"}
+ ),
"name": "img",
"in": "query",
},
import os
import shutil
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.additional_responses.tutorial004 import app
},
{
"required": False,
- "schema": {"title": "Img", "type": "boolean"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "boolean"}, {"type": "null"}],
+ "title": "Img",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Img", "type": "boolean"}
+ ),
"name": "img",
"in": "query",
},
from docs_src.async_sql_databases.tutorial001 import app
+from ...utils import needs_pydanticv1
+
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_create_read():
with TestClient(app) as client:
note = {"text": "Foo bar", "completed": False}
+from dirty_equals import IsOneOf
from fastapi.testclient import TestClient
from docs_src.behind_a_proxy.tutorial003 import app
assert response.json() == {"message": "Hello World", "root_path": "/api/v1"}
-def test_openapi():
+def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == {
"info": {"title": "FastAPI", "version": "0.1.0"},
"servers": [
{"url": "/api/v1"},
- {"url": "https://stag.example.com", "description": "Staging environment"},
{
- "url": "https://prod.example.com",
+ "url": IsOneOf(
+ "https://stag.example.com/",
+ # TODO: remove when deprecating Pydantic v1
+ "https://stag.example.com",
+ ),
+ "description": "Staging environment",
+ },
+ {
+ "url": IsOneOf(
+ "https://prod.example.com/",
+ # TODO: remove when deprecating Pydantic v1
+ "https://prod.example.com",
+ ),
"description": "Production environment",
},
],
+from dirty_equals import IsOneOf
from fastapi.testclient import TestClient
from docs_src.behind_a_proxy.tutorial004 import app
assert response.json() == {"message": "Hello World", "root_path": "/api/v1"}
-def test_openapi():
+def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == {
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"servers": [
- {"url": "https://stag.example.com", "description": "Staging environment"},
{
- "url": "https://prod.example.com",
+ "url": IsOneOf(
+ "https://stag.example.com/",
+ # TODO: remove when deprecating Pydantic v1
+ "https://stag.example.com",
+ ),
+ "description": "Staging environment",
+ },
+ {
+ "url": IsOneOf(
+ "https://prod.example.com/",
+ # TODO: remove when deprecating Pydantic v1
+ "https://prod.example.com",
+ ),
"description": "Production environment",
},
],
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.bigger_applications.app.main import app
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.bigger_applications.app.main import app
+ client = TestClient(app)
+ return client
-no_jessica = {
- "detail": [
+
+def test_users_token_jessica(client: TestClient):
+ response = client.get("/users?token=jessica")
+ assert response.status_code == 200
+ assert response.json() == [{"username": "Rick"}, {"username": "Morty"}]
+
+
+def test_users_with_no_token(client: TestClient):
+ response = client.get("/users")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["query", "token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
-
-
-@pytest.mark.parametrize(
- "path,expected_status,expected_response,headers",
- [
- (
- "/users?token=jessica",
- 200,
- [{"username": "Rick"}, {"username": "Morty"}],
- {},
- ),
- ("/users", 422, no_jessica, {}),
- ("/users/foo?token=jessica", 200, {"username": "foo"}, {}),
- ("/users/foo", 422, no_jessica, {}),
- ("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}),
- ("/users/me", 422, no_jessica, {}),
- (
- "/users?token=monica",
- 400,
- {"detail": "No Jessica token provided"},
- {},
- ),
- (
- "/items?token=jessica",
- 200,
- {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}},
- {"X-Token": "fake-super-secret-token"},
- ),
- ("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
- (
- "/items/plumbus?token=jessica",
- 200,
- {"name": "Plumbus", "item_id": "plumbus"},
- {"X-Token": "fake-super-secret-token"},
- ),
- (
- "/items/bar?token=jessica",
- 404,
- {"detail": "Item not found"},
- {"X-Token": "fake-super-secret-token"},
- ),
- ("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
- (
- "/items?token=jessica",
- 400,
- {"detail": "X-Token header invalid"},
- {"X-Token": "invalid"},
- ),
- (
- "/items/bar?token=jessica",
- 400,
- {"detail": "X-Token header invalid"},
- {"X-Token": "invalid"},
- ),
- (
- "/items?token=jessica",
- 422,
- {
- "detail": [
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- {},
- ),
- (
- "/items/plumbus?token=jessica",
- 422,
- {
- "detail": [
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- {},
- ),
- ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}),
- ("/", 422, no_jessica, {}),
- ],
-)
-def test_get_path(path, expected_status, expected_response, headers):
- response = client.get(path, headers=headers)
- assert response.status_code == expected_status
- assert response.json() == expected_response
-
-
-def test_put_no_header():
- response = client.put("/items/foo")
- assert response.status_code == 422, response.text
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_users_foo_token_jessica(client: TestClient):
+ response = client.get("/users/foo?token=jessica")
+ assert response.status_code == 200
+ assert response.json() == {"username": "foo"}
+
+
+def test_users_foo_with_no_token(client: TestClient):
+ response = client.get("/users/foo")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_users_me_token_jessica(client: TestClient):
+ response = client.get("/users/me?token=jessica")
+ assert response.status_code == 200
+ assert response.json() == {"username": "fakecurrentuser"}
+
+
+def test_users_me_with_no_token(client: TestClient):
+ response = client.get("/users/me")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_users_token_monica_with_no_jessica(client: TestClient):
+ response = client.get("/users?token=monica")
+ assert response.status_code == 400
+ assert response.json() == {"detail": "No Jessica token provided"}
+
+
+def test_items_token_jessica(client: TestClient):
+ response = client.get(
+ "/items?token=jessica", headers={"X-Token": "fake-super-secret-token"}
+ )
+ assert response.status_code == 200
assert response.json() == {
- "detail": [
- {
- "loc": ["query", "token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
+ "plumbus": {"name": "Plumbus"},
+ "gun": {"name": "Portal Gun"},
}
-def test_put_invalid_header():
+def test_items_with_no_token_jessica(client: TestClient):
+ response = client.get("/items", headers={"X-Token": "fake-super-secret-token"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_items_plumbus_token_jessica(client: TestClient):
+ response = client.get(
+ "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"}
+ )
+ assert response.status_code == 200
+ assert response.json() == {"name": "Plumbus", "item_id": "plumbus"}
+
+
+def test_items_bar_token_jessica(client: TestClient):
+ response = client.get(
+ "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"}
+ )
+ assert response.status_code == 404
+ assert response.json() == {"detail": "Item not found"}
+
+
+def test_items_plumbus_with_no_token(client: TestClient):
+ response = client.get(
+ "/items/plumbus", headers={"X-Token": "fake-super-secret-token"}
+ )
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_items_with_invalid_token(client: TestClient):
+ response = client.get("/items?token=jessica", headers={"X-Token": "invalid"})
+ assert response.status_code == 400
+ assert response.json() == {"detail": "X-Token header invalid"}
+
+
+def test_items_bar_with_invalid_token(client: TestClient):
+ response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"})
+ assert response.status_code == 400
+ assert response.json() == {"detail": "X-Token header invalid"}
+
+
+def test_items_with_missing_x_token_header(client: TestClient):
+ response = client.get("/items?token=jessica")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_items_plumbus_with_missing_x_token_header(client: TestClient):
+ response = client.get("/items/plumbus?token=jessica")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_root_token_jessica(client: TestClient):
+ response = client.get("/?token=jessica")
+ assert response.status_code == 200
+ assert response.json() == {"message": "Hello Bigger Applications!"}
+
+
+def test_root_with_no_token(client: TestClient):
+ response = client.get("/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_put_no_header(client: TestClient):
+ response = client.put("/items/foo")
+ assert response.status_code == 422, response.text
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_put_invalid_header(client: TestClient):
response = client.put("/items/foo", headers={"X-Token": "invalid"})
assert response.status_code == 400, response.text
assert response.json() == {"detail": "X-Token header invalid"}
-def test_put():
+def test_put(client: TestClient):
response = client.put(
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"}
-def test_put_forbidden():
+def test_put_forbidden(client: TestClient):
response = client.put(
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
assert response.json() == {"detail": "You can only update the item: plumbus"}
-def test_admin():
+def test_admin(client: TestClient):
response = client.post(
"/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
assert response.json() == {"message": "Admin getting schwifty"}
-def test_admin_invalid_header():
+def test_admin_invalid_header(client: TestClient):
response = client.post("/admin/", headers={"X-Token": "invalid"})
assert response.status_code == 400, response.text
assert response.json() == {"detail": "X-Token header invalid"}
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.bigger_applications.app_an.main import app
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.bigger_applications.app_an.main import app
+ client = TestClient(app)
+ return client
-no_jessica = {
- "detail": [
+
+def test_users_token_jessica(client: TestClient):
+ response = client.get("/users?token=jessica")
+ assert response.status_code == 200
+ assert response.json() == [{"username": "Rick"}, {"username": "Morty"}]
+
+
+def test_users_with_no_token(client: TestClient):
+ response = client.get("/users")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["query", "token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
-
-
-@pytest.mark.parametrize(
- "path,expected_status,expected_response,headers",
- [
- (
- "/users?token=jessica",
- 200,
- [{"username": "Rick"}, {"username": "Morty"}],
- {},
- ),
- ("/users", 422, no_jessica, {}),
- ("/users/foo?token=jessica", 200, {"username": "foo"}, {}),
- ("/users/foo", 422, no_jessica, {}),
- ("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}),
- ("/users/me", 422, no_jessica, {}),
- (
- "/users?token=monica",
- 400,
- {"detail": "No Jessica token provided"},
- {},
- ),
- (
- "/items?token=jessica",
- 200,
- {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}},
- {"X-Token": "fake-super-secret-token"},
- ),
- ("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
- (
- "/items/plumbus?token=jessica",
- 200,
- {"name": "Plumbus", "item_id": "plumbus"},
- {"X-Token": "fake-super-secret-token"},
- ),
- (
- "/items/bar?token=jessica",
- 404,
- {"detail": "Item not found"},
- {"X-Token": "fake-super-secret-token"},
- ),
- ("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
- (
- "/items?token=jessica",
- 400,
- {"detail": "X-Token header invalid"},
- {"X-Token": "invalid"},
- ),
- (
- "/items/bar?token=jessica",
- 400,
- {"detail": "X-Token header invalid"},
- {"X-Token": "invalid"},
- ),
- (
- "/items?token=jessica",
- 422,
- {
- "detail": [
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- {},
- ),
- (
- "/items/plumbus?token=jessica",
- 422,
- {
- "detail": [
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- {},
- ),
- ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}),
- ("/", 422, no_jessica, {}),
- ],
-)
-def test_get_path(path, expected_status, expected_response, headers):
- response = client.get(path, headers=headers)
- assert response.status_code == expected_status
- assert response.json() == expected_response
-
-
-def test_put_no_header():
- response = client.put("/items/foo")
- assert response.status_code == 422, response.text
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_users_foo_token_jessica(client: TestClient):
+ response = client.get("/users/foo?token=jessica")
+ assert response.status_code == 200
+ assert response.json() == {"username": "foo"}
+
+
+def test_users_foo_with_no_token(client: TestClient):
+ response = client.get("/users/foo")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_users_me_token_jessica(client: TestClient):
+ response = client.get("/users/me?token=jessica")
+ assert response.status_code == 200
+ assert response.json() == {"username": "fakecurrentuser"}
+
+
+def test_users_me_with_no_token(client: TestClient):
+ response = client.get("/users/me")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_users_token_monica_with_no_jessica(client: TestClient):
+ response = client.get("/users?token=monica")
+ assert response.status_code == 400
+ assert response.json() == {"detail": "No Jessica token provided"}
+
+
+def test_items_token_jessica(client: TestClient):
+ response = client.get(
+ "/items?token=jessica", headers={"X-Token": "fake-super-secret-token"}
+ )
+ assert response.status_code == 200
assert response.json() == {
- "detail": [
- {
- "loc": ["query", "token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
+ "plumbus": {"name": "Plumbus"},
+ "gun": {"name": "Portal Gun"},
}
-def test_put_invalid_header():
+def test_items_with_no_token_jessica(client: TestClient):
+ response = client.get("/items", headers={"X-Token": "fake-super-secret-token"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_items_plumbus_token_jessica(client: TestClient):
+ response = client.get(
+ "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"}
+ )
+ assert response.status_code == 200
+ assert response.json() == {"name": "Plumbus", "item_id": "plumbus"}
+
+
+def test_items_bar_token_jessica(client: TestClient):
+ response = client.get(
+ "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"}
+ )
+ assert response.status_code == 404
+ assert response.json() == {"detail": "Item not found"}
+
+
+def test_items_plumbus_with_no_token(client: TestClient):
+ response = client.get(
+ "/items/plumbus", headers={"X-Token": "fake-super-secret-token"}
+ )
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_items_with_invalid_token(client: TestClient):
+ response = client.get("/items?token=jessica", headers={"X-Token": "invalid"})
+ assert response.status_code == 400
+ assert response.json() == {"detail": "X-Token header invalid"}
+
+
+def test_items_bar_with_invalid_token(client: TestClient):
+ response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"})
+ assert response.status_code == 400
+ assert response.json() == {"detail": "X-Token header invalid"}
+
+
+def test_items_with_missing_x_token_header(client: TestClient):
+ response = client.get("/items?token=jessica")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_items_plumbus_with_missing_x_token_header(client: TestClient):
+ response = client.get("/items/plumbus?token=jessica")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_root_token_jessica(client: TestClient):
+ response = client.get("/?token=jessica")
+ assert response.status_code == 200
+ assert response.json() == {"message": "Hello Bigger Applications!"}
+
+
+def test_root_with_no_token(client: TestClient):
+ response = client.get("/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_put_no_header(client: TestClient):
+ response = client.put("/items/foo")
+ assert response.status_code == 422, response.text
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_put_invalid_header(client: TestClient):
response = client.put("/items/foo", headers={"X-Token": "invalid"})
assert response.status_code == 400, response.text
assert response.json() == {"detail": "X-Token header invalid"}
-def test_put():
+def test_put(client: TestClient):
response = client.put(
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"}
-def test_put_forbidden():
+def test_put_forbidden(client: TestClient):
response = client.put(
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
assert response.json() == {"detail": "You can only update the item: plumbus"}
-def test_admin():
+def test_admin(client: TestClient):
response = client.post(
"/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"}
)
assert response.json() == {"message": "Admin getting schwifty"}
-def test_admin_invalid_header():
+def test_admin_invalid_header(client: TestClient):
response = client.post("/admin/", headers={"X-Token": "invalid"})
assert response.status_code == 400, response.text
assert response.json() == {"detail": "X-Token header invalid"}
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
-no_jessica = {
- "detail": [
- {
- "loc": ["query", "token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
-
@pytest.fixture(name="client")
def get_client():
@needs_py39
-@pytest.mark.parametrize(
- "path,expected_status,expected_response,headers",
- [
- (
- "/users?token=jessica",
- 200,
- [{"username": "Rick"}, {"username": "Morty"}],
- {},
- ),
- ("/users", 422, no_jessica, {}),
- ("/users/foo?token=jessica", 200, {"username": "foo"}, {}),
- ("/users/foo", 422, no_jessica, {}),
- ("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}),
- ("/users/me", 422, no_jessica, {}),
- (
- "/users?token=monica",
- 400,
- {"detail": "No Jessica token provided"},
- {},
- ),
- (
- "/items?token=jessica",
- 200,
- {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}},
- {"X-Token": "fake-super-secret-token"},
- ),
- ("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
- (
- "/items/plumbus?token=jessica",
- 200,
- {"name": "Plumbus", "item_id": "plumbus"},
- {"X-Token": "fake-super-secret-token"},
- ),
- (
- "/items/bar?token=jessica",
- 404,
- {"detail": "Item not found"},
- {"X-Token": "fake-super-secret-token"},
- ),
- ("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
- (
- "/items?token=jessica",
- 400,
- {"detail": "X-Token header invalid"},
- {"X-Token": "invalid"},
- ),
- (
- "/items/bar?token=jessica",
- 400,
- {"detail": "X-Token header invalid"},
- {"X-Token": "invalid"},
- ),
- (
- "/items?token=jessica",
- 422,
- {
- "detail": [
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- {},
- ),
- (
- "/items/plumbus?token=jessica",
- 422,
- {
- "detail": [
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
- },
- {},
- ),
- ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}),
- ("/", 422, no_jessica, {}),
- ],
-)
-def test_get_path(
- path, expected_status, expected_response, headers, client: TestClient
-):
- response = client.get(path, headers=headers)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_users_token_jessica(client: TestClient):
+ response = client.get("/users?token=jessica")
+ assert response.status_code == 200
+ assert response.json() == [{"username": "Rick"}, {"username": "Morty"}]
+
+
+@needs_py39
+def test_users_with_no_token(client: TestClient):
+ response = client.get("/users")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+@needs_py39
+def test_users_foo_token_jessica(client: TestClient):
+ response = client.get("/users/foo?token=jessica")
+ assert response.status_code == 200
+ assert response.json() == {"username": "foo"}
+
+
+@needs_py39
+def test_users_foo_with_no_token(client: TestClient):
+ response = client.get("/users/foo")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+@needs_py39
+def test_users_me_token_jessica(client: TestClient):
+ response = client.get("/users/me?token=jessica")
+ assert response.status_code == 200
+ assert response.json() == {"username": "fakecurrentuser"}
+
+
+@needs_py39
+def test_users_me_with_no_token(client: TestClient):
+ response = client.get("/users/me")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+@needs_py39
+def test_users_token_monica_with_no_jessica(client: TestClient):
+ response = client.get("/users?token=monica")
+ assert response.status_code == 400
+ assert response.json() == {"detail": "No Jessica token provided"}
+
+
+@needs_py39
+def test_items_token_jessica(client: TestClient):
+ response = client.get(
+ "/items?token=jessica", headers={"X-Token": "fake-super-secret-token"}
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "plumbus": {"name": "Plumbus"},
+ "gun": {"name": "Portal Gun"},
+ }
+
+
+@needs_py39
+def test_items_with_no_token_jessica(client: TestClient):
+ response = client.get("/items", headers={"X-Token": "fake-super-secret-token"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+@needs_py39
+def test_items_plumbus_token_jessica(client: TestClient):
+ response = client.get(
+ "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"}
+ )
+ assert response.status_code == 200
+ assert response.json() == {"name": "Plumbus", "item_id": "plumbus"}
+
+
+@needs_py39
+def test_items_bar_token_jessica(client: TestClient):
+ response = client.get(
+ "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"}
+ )
+ assert response.status_code == 404
+ assert response.json() == {"detail": "Item not found"}
+
+
+@needs_py39
+def test_items_plumbus_with_no_token(client: TestClient):
+ response = client.get(
+ "/items/plumbus", headers={"X-Token": "fake-super-secret-token"}
+ )
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+@needs_py39
+def test_items_with_invalid_token(client: TestClient):
+ response = client.get("/items?token=jessica", headers={"X-Token": "invalid"})
+ assert response.status_code == 400
+ assert response.json() == {"detail": "X-Token header invalid"}
+
+
+@needs_py39
+def test_items_bar_with_invalid_token(client: TestClient):
+ response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"})
+ assert response.status_code == 400
+ assert response.json() == {"detail": "X-Token header invalid"}
+
+
+@needs_py39
+def test_items_with_missing_x_token_header(client: TestClient):
+ response = client.get("/items?token=jessica")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@needs_py39
+def test_items_plumbus_with_missing_x_token_header(client: TestClient):
+ response = client.get("/items/plumbus?token=jessica")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@needs_py39
+def test_root_token_jessica(client: TestClient):
+ response = client.get("/?token=jessica")
+ assert response.status_code == 200
+ assert response.json() == {"message": "Hello Bigger Applications!"}
+
+
+@needs_py39
+def test_root_with_no_token(client: TestClient):
+ response = client.get("/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
@needs_py39
def test_put_no_header(client: TestClient):
response = client.put("/items/foo")
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["query", "token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
@needs_py39
from unittest.mock import patch
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.body.tutorial001 import app
-client = TestClient(app)
+@pytest.fixture
+def client():
+ from docs_src.body.tutorial001 import app
+ client = TestClient(app)
+ return client
-price_missing = {
- "detail": [
+
+def test_body_float(client: TestClient):
+ response = client.post("/items/", json={"name": "Foo", "price": 50.5})
+ assert response.status_code == 200
+ assert response.json() == {
+ "name": "Foo",
+ "price": 50.5,
+ "description": None,
+ "tax": None,
+ }
+
+
+def test_post_with_str_float(client: TestClient):
+ response = client.post("/items/", json={"name": "Foo", "price": "50.5"})
+ assert response.status_code == 200
+ assert response.json() == {
+ "name": "Foo",
+ "price": 50.5,
+ "description": None,
+ "tax": None,
+ }
+
+
+def test_post_with_str_float_description(client: TestClient):
+ response = client.post(
+ "/items/", json={"name": "Foo", "price": "50.5", "description": "Some Foo"}
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "name": "Foo",
+ "price": 50.5,
+ "description": "Some Foo",
+ "tax": None,
+ }
+
+
+def test_post_with_str_float_description_tax(client: TestClient):
+ response = client.post(
+ "/items/",
+ json={"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3},
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "name": "Foo",
+ "price": 50.5,
+ "description": "Some Foo",
+ "tax": 0.3,
+ }
+
+
+def test_post_with_only_name(client: TestClient):
+ response = client.post("/items/", json={"name": "Foo"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "price"],
- "msg": "field required",
- "type": "value_error.missing",
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "price"],
+ "msg": "Field required",
+ "input": {"name": "Foo"},
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
}
- ]
-}
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "price"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
-price_not_float = {
- "detail": [
+def test_post_with_only_name_price(client: TestClient):
+ response = client.post("/items/", json={"name": "Foo", "price": "twenty"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "price"],
- "msg": "value is not a valid float",
- "type": "type_error.float",
+ "detail": [
+ {
+ "type": "float_parsing",
+ "loc": ["body", "price"],
+ "msg": "Input should be a valid number, unable to parse string as a number",
+ "input": "twenty",
+ "url": match_pydantic_error_url("float_parsing"),
+ }
+ ]
}
- ]
-}
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "price"],
+ "msg": "value is not a valid float",
+ "type": "type_error.float",
+ }
+ ]
+ }
+ )
-name_price_missing = {
- "detail": [
+
+def test_post_with_no_data(client: TestClient):
+ response = client.post("/items/", json={})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "name"],
- "msg": "field required",
- "type": "value_error.missing",
- },
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "name"],
+ "msg": "Field required",
+ "input": {},
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "price"],
+ "msg": "Field required",
+ "input": {},
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["body", "price"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
-
-body_missing = {
- "detail": [
- {"loc": ["body"], "msg": "field required", "type": "value_error.missing"}
- ]
-}
-
-
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/",
- {"name": "Foo", "price": 50.5},
- 200,
- {"name": "Foo", "price": 50.5, "description": None, "tax": None},
- ),
- (
- "/items/",
- {"name": "Foo", "price": "50.5"},
- 200,
- {"name": "Foo", "price": 50.5, "description": None, "tax": None},
- ),
- (
- "/items/",
- {"name": "Foo", "price": "50.5", "description": "Some Foo"},
- 200,
- {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": None},
- ),
- (
- "/items/",
- {"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3},
- 200,
- {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": 0.3},
- ),
- ("/items/", {"name": "Foo"}, 422, price_missing),
- ("/items/", {"name": "Foo", "price": "twenty"}, 422, price_not_float),
- ("/items/", {}, 422, name_price_missing),
- ("/items/", None, 422, body_missing),
- ],
-)
-def test_post_body(path, body, expected_status, expected_response):
- response = client.post(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
-
-
-def test_post_broken_body():
+ "detail": [
+ {
+ "loc": ["body", "name"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "price"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_post_with_none(client: TestClient):
+ response = client.post("/items/", json=None)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+def test_post_broken_body(client: TestClient):
response = client.post(
"/items/",
headers={"content-type": "application/json"},
content="{some broken json}",
)
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["body", 1],
- "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)",
- "type": "value_error.jsondecode",
- "ctx": {
- "msg": "Expecting property name enclosed in double quotes",
- "doc": "{some broken json}",
- "pos": 1,
- "lineno": 1,
- "colno": 2,
- },
- }
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "json_invalid",
+ "loc": ["body", 1],
+ "msg": "JSON decode error",
+ "input": {},
+ "ctx": {
+ "error": "Expecting property name enclosed in double quotes"
+ },
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", 1],
+ "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)",
+ "type": "value_error.jsondecode",
+ "ctx": {
+ "msg": "Expecting property name enclosed in double quotes",
+ "doc": "{some broken json}",
+ "pos": 1,
+ "lineno": 1,
+ "colno": 2,
+ },
+ }
+ ]
+ }
+ )
-def test_post_form_for_json():
+def test_post_form_for_json(client: TestClient):
response = client.post("/items/", data={"name": "Foo", "price": 50.5})
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["body"],
- "msg": "value is not a valid dict",
- "type": "type_error.dict",
- }
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "model_attributes_type",
+ "loc": ["body"],
+ "msg": "Input should be a valid dictionary or object to extract fields from",
+ "input": "name=Foo&price=50.5",
+ "url": match_pydantic_error_url("model_attributes_type"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "value is not a valid dict",
+ "type": "type_error.dict",
+ }
+ ]
+ }
+ )
-def test_explicit_content_type():
+def test_explicit_content_type(client: TestClient):
response = client.post(
"/items/",
content='{"name": "Foo", "price": 50.5}',
assert response.status_code == 200, response.text
-def test_geo_json():
+def test_geo_json(client: TestClient):
response = client.post(
"/items/",
content='{"name": "Foo", "price": 50.5}',
assert response.status_code == 200, response.text
-def test_no_content_type_is_json():
+def test_no_content_type_is_json(client: TestClient):
response = client.post(
"/items/",
content='{"name": "Foo", "price": 50.5}',
}
-def test_wrong_headers():
+def test_wrong_headers(client: TestClient):
data = '{"name": "Foo", "price": 50.5}'
- invalid_dict = {
- "detail": [
- {
- "loc": ["body"],
- "msg": "value is not a valid dict",
- "type": "type_error.dict",
- }
- ]
- }
-
response = client.post(
"/items/", content=data, headers={"Content-Type": "text/plain"}
)
assert response.status_code == 422, response.text
- assert response.json() == invalid_dict
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "model_attributes_type",
+ "loc": ["body"],
+ "msg": "Input should be a valid dictionary or object to extract fields from",
+ "input": '{"name": "Foo", "price": 50.5}',
+ "url": match_pydantic_error_url(
+ "model_attributes_type"
+ ), # "https://errors.pydantic.dev/0.38.0/v/dict_attributes_type",
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "value is not a valid dict",
+ "type": "type_error.dict",
+ }
+ ]
+ }
+ )
response = client.post(
"/items/", content=data, headers={"Content-Type": "application/geo+json-seq"}
)
assert response.status_code == 422, response.text
- assert response.json() == invalid_dict
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "model_attributes_type",
+ "loc": ["body"],
+ "msg": "Input should be a valid dictionary or object to extract fields from",
+ "input": '{"name": "Foo", "price": 50.5}',
+ "url": match_pydantic_error_url("model_attributes_type"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "value is not a valid dict",
+ "type": "type_error.dict",
+ }
+ ]
+ }
+ )
response = client.post(
"/items/", content=data, headers={"Content-Type": "application/not-really-json"}
)
assert response.status_code == 422, response.text
- assert response.json() == invalid_dict
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "model_attributes_type",
+ "loc": ["body"],
+ "msg": "Input should be a valid dictionary or object to extract fields from",
+ "input": '{"name": "Foo", "price": 50.5}',
+ "url": match_pydantic_error_url("model_attributes_type"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "value is not a valid dict",
+ "type": "type_error.dict",
+ }
+ ]
+ }
+ )
-def test_other_exceptions():
+def test_other_exceptions(client: TestClient):
with patch("json.loads", side_effect=Exception):
response = client.post("/items/", json={"test": "test2"})
assert response.status_code == 400, response.text
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"ValidationError": {
from unittest.mock import patch
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310
return client
-price_missing = {
- "detail": [
+@needs_py310
+def test_body_float(client: TestClient):
+ response = client.post("/items/", json={"name": "Foo", "price": 50.5})
+ assert response.status_code == 200
+ assert response.json() == {
+ "name": "Foo",
+ "price": 50.5,
+ "description": None,
+ "tax": None,
+ }
+
+
+@needs_py310
+def test_post_with_str_float(client: TestClient):
+ response = client.post("/items/", json={"name": "Foo", "price": "50.5"})
+ assert response.status_code == 200
+ assert response.json() == {
+ "name": "Foo",
+ "price": 50.5,
+ "description": None,
+ "tax": None,
+ }
+
+
+@needs_py310
+def test_post_with_str_float_description(client: TestClient):
+ response = client.post(
+ "/items/", json={"name": "Foo", "price": "50.5", "description": "Some Foo"}
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "name": "Foo",
+ "price": 50.5,
+ "description": "Some Foo",
+ "tax": None,
+ }
+
+
+@needs_py310
+def test_post_with_str_float_description_tax(client: TestClient):
+ response = client.post(
+ "/items/",
+ json={"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3},
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "name": "Foo",
+ "price": 50.5,
+ "description": "Some Foo",
+ "tax": 0.3,
+ }
+
+
+@needs_py310
+def test_post_with_only_name(client: TestClient):
+ response = client.post("/items/", json={"name": "Foo"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "price"],
- "msg": "field required",
- "type": "value_error.missing",
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "price"],
+ "msg": "Field required",
+ "input": {"name": "Foo"},
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
}
- ]
-}
-
-price_not_float = {
- "detail": [
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["body", "price"],
- "msg": "value is not a valid float",
- "type": "type_error.float",
+ "detail": [
+ {
+ "loc": ["body", "price"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
}
- ]
-}
+ )
+
-name_price_missing = {
- "detail": [
+@needs_py310
+def test_post_with_only_name_price(client: TestClient):
+ response = client.post("/items/", json={"name": "Foo", "price": "twenty"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "name"],
- "msg": "field required",
- "type": "value_error.missing",
- },
+ "detail": [
+ {
+ "type": "float_parsing",
+ "loc": ["body", "price"],
+ "msg": "Input should be a valid number, unable to parse string as a number",
+ "input": "twenty",
+ "url": match_pydantic_error_url("float_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["body", "price"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
+ "detail": [
+ {
+ "loc": ["body", "price"],
+ "msg": "value is not a valid float",
+ "type": "type_error.float",
+ }
+ ]
+ }
+ )
-body_missing = {
- "detail": [
- {"loc": ["body"], "msg": "field required", "type": "value_error.missing"}
- ]
-}
+
+@needs_py310
+def test_post_with_no_data(client: TestClient):
+ response = client.post("/items/", json={})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "name"],
+ "msg": "Field required",
+ "input": {},
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "price"],
+ "msg": "Field required",
+ "input": {},
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "name"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "price"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
@needs_py310
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/",
- {"name": "Foo", "price": 50.5},
- 200,
- {"name": "Foo", "price": 50.5, "description": None, "tax": None},
- ),
- (
- "/items/",
- {"name": "Foo", "price": "50.5"},
- 200,
- {"name": "Foo", "price": 50.5, "description": None, "tax": None},
- ),
- (
- "/items/",
- {"name": "Foo", "price": "50.5", "description": "Some Foo"},
- 200,
- {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": None},
- ),
- (
- "/items/",
- {"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3},
- 200,
- {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": 0.3},
- ),
- ("/items/", {"name": "Foo"}, 422, price_missing),
- ("/items/", {"name": "Foo", "price": "twenty"}, 422, price_not_float),
- ("/items/", {}, 422, name_price_missing),
- ("/items/", None, 422, body_missing),
- ],
-)
-def test_post_body(path, body, expected_status, expected_response, client: TestClient):
- response = client.post(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_post_with_none(client: TestClient):
+ response = client.post("/items/", json=None)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
@needs_py310
content="{some broken json}",
)
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["body", 1],
- "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)",
- "type": "value_error.jsondecode",
- "ctx": {
- "msg": "Expecting property name enclosed in double quotes",
- "doc": "{some broken json}",
- "pos": 1,
- "lineno": 1,
- "colno": 2,
- },
- }
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "json_invalid",
+ "loc": ["body", 1],
+ "msg": "JSON decode error",
+ "input": {},
+ "ctx": {
+ "error": "Expecting property name enclosed in double quotes"
+ },
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", 1],
+ "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)",
+ "type": "value_error.jsondecode",
+ "ctx": {
+ "msg": "Expecting property name enclosed in double quotes",
+ "doc": "{some broken json}",
+ "pos": 1,
+ "lineno": 1,
+ "colno": 2,
+ },
+ }
+ ]
+ }
+ )
@needs_py310
def test_post_form_for_json(client: TestClient):
response = client.post("/items/", data={"name": "Foo", "price": 50.5})
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["body"],
- "msg": "value is not a valid dict",
- "type": "type_error.dict",
- }
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "model_attributes_type",
+ "loc": ["body"],
+ "msg": "Input should be a valid dictionary or object to extract fields from",
+ "input": "name=Foo&price=50.5",
+ "url": match_pydantic_error_url("model_attributes_type"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "value is not a valid dict",
+ "type": "type_error.dict",
+ }
+ ]
+ }
+ )
@needs_py310
@needs_py310
def test_wrong_headers(client: TestClient):
data = '{"name": "Foo", "price": 50.5}'
- invalid_dict = {
- "detail": [
- {
- "loc": ["body"],
- "msg": "value is not a valid dict",
- "type": "type_error.dict",
- }
- ]
- }
-
response = client.post(
"/items/", content=data, headers={"Content-Type": "text/plain"}
)
assert response.status_code == 422, response.text
- assert response.json() == invalid_dict
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "model_attributes_type",
+ "loc": ["body"],
+ "msg": "Input should be a valid dictionary or object to extract fields from",
+ "input": '{"name": "Foo", "price": 50.5}',
+ "url": match_pydantic_error_url("model_attributes_type"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "value is not a valid dict",
+ "type": "type_error.dict",
+ }
+ ]
+ }
+ )
response = client.post(
"/items/", content=data, headers={"Content-Type": "application/geo+json-seq"}
)
assert response.status_code == 422, response.text
- assert response.json() == invalid_dict
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "model_attributes_type",
+ "loc": ["body"],
+ "msg": "Input should be a valid dictionary or object to extract fields from",
+ "input": '{"name": "Foo", "price": 50.5}',
+ "url": match_pydantic_error_url("model_attributes_type"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "value is not a valid dict",
+ "type": "type_error.dict",
+ }
+ ]
+ }
+ )
response = client.post(
"/items/", content=data, headers={"Content-Type": "application/not-really-json"}
)
assert response.status_code == 422, response.text
- assert response.json() == invalid_dict
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "model_attributes_type",
+ "loc": ["body"],
+ "msg": "Input should be a valid dictionary or object to extract fields from",
+ "input": '{"name": "Foo", "price": 50.5}',
+ "url": match_pydantic_error_url("model_attributes_type"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "value is not a valid dict",
+ "type": "type_error.dict",
+ }
+ ]
+ }
+ )
@needs_py310
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.body_fields.tutorial001 import app
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.body_fields.tutorial001 import app
+ client = TestClient(app)
+ return client
-price_not_greater = {
- "detail": [
- {
- "ctx": {"limit_value": 0},
- "loc": ["body", "item", "price"],
- "msg": "ensure this value is greater than 0",
- "type": "value_error.number.not_gt",
- }
- ]
-}
+
+def test_items_5(client: TestClient):
+ response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 5,
+ "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
+ }
+
+
+def test_items_6(client: TestClient):
+ response = client.put(
+ "/items/6",
+ json={
+ "item": {
+ "name": "Bar",
+ "price": 0.2,
+ "description": "Some bar",
+ "tax": "5.4",
+ }
+ },
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 6,
+ "item": {
+ "name": "Bar",
+ "price": 0.2,
+ "description": "Some bar",
+ "tax": 5.4,
+ },
+ }
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/5",
- {"item": {"name": "Foo", "price": 3.0}},
- 200,
- {
- "item_id": 5,
- "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
- },
- ),
- (
- "/items/6",
- {
- "item": {
- "name": "Bar",
- "price": 0.2,
- "description": "Some bar",
- "tax": "5.4",
+def test_invalid_price(client: TestClient):
+ response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "greater_than",
+ "loc": ["body", "item", "price"],
+ "msg": "Input should be greater than 0",
+ "input": -3.0,
+ "ctx": {"gt": 0.0},
+ "url": match_pydantic_error_url("greater_than"),
}
- },
- 200,
- {
- "item_id": 6,
- "item": {
- "name": "Bar",
- "price": 0.2,
- "description": "Some bar",
- "tax": 5.4,
- },
- },
- ),
- ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater),
- ],
-)
-def test(path, body, expected_status, expected_response):
- response = client.put(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "ctx": {"limit_value": 0},
+ "loc": ["body", "item", "price"],
+ "msg": "ensure this value is greater than 0",
+ "type": "value_error.number.not_gt",
+ }
+ ]
+ }
+ )
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
- "description": {
- "title": "The description of the item",
- "maxLength": 300,
- "type": "string",
- },
+ "description": IsDict(
+ {
+ "title": "The description of the item",
+ "anyOf": [
+ {"maxLength": 300, "type": "string"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "The description of the item",
+ "maxLength": 300,
+ "type": "string",
+ }
+ ),
"price": {
"title": "Price",
"exclusiveMinimum": 0.0,
"type": "number",
"description": "The price must be greater than zero",
},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"Body_update_item_items__item_id__put": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.body_fields.tutorial001_an import app
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.body_fields.tutorial001_an import app
+ client = TestClient(app)
+ return client
-price_not_greater = {
- "detail": [
- {
- "ctx": {"limit_value": 0},
- "loc": ["body", "item", "price"],
- "msg": "ensure this value is greater than 0",
- "type": "value_error.number.not_gt",
- }
- ]
-}
+
+def test_items_5(client: TestClient):
+ response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 5,
+ "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
+ }
+
+
+def test_items_6(client: TestClient):
+ response = client.put(
+ "/items/6",
+ json={
+ "item": {
+ "name": "Bar",
+ "price": 0.2,
+ "description": "Some bar",
+ "tax": "5.4",
+ }
+ },
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 6,
+ "item": {
+ "name": "Bar",
+ "price": 0.2,
+ "description": "Some bar",
+ "tax": 5.4,
+ },
+ }
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/5",
- {"item": {"name": "Foo", "price": 3.0}},
- 200,
- {
- "item_id": 5,
- "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
- },
- ),
- (
- "/items/6",
- {
- "item": {
- "name": "Bar",
- "price": 0.2,
- "description": "Some bar",
- "tax": "5.4",
+def test_invalid_price(client: TestClient):
+ response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "greater_than",
+ "loc": ["body", "item", "price"],
+ "msg": "Input should be greater than 0",
+ "input": -3.0,
+ "ctx": {"gt": 0.0},
+ "url": match_pydantic_error_url("greater_than"),
}
- },
- 200,
- {
- "item_id": 6,
- "item": {
- "name": "Bar",
- "price": 0.2,
- "description": "Some bar",
- "tax": 5.4,
- },
- },
- ),
- ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater),
- ],
-)
-def test(path, body, expected_status, expected_response):
- response = client.put(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "ctx": {"limit_value": 0},
+ "loc": ["body", "item", "price"],
+ "msg": "ensure this value is greater than 0",
+ "type": "value_error.number.not_gt",
+ }
+ ]
+ }
+ )
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
- "description": {
- "title": "The description of the item",
- "maxLength": 300,
- "type": "string",
- },
+ "description": IsDict(
+ {
+ "title": "The description of the item",
+ "anyOf": [
+ {"maxLength": 300, "type": "string"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "The description of the item",
+ "maxLength": 300,
+ "type": "string",
+ }
+ ),
"price": {
"title": "Price",
"exclusiveMinimum": 0.0,
"type": "number",
"description": "The price must be greater than zero",
},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"Body_update_item_items__item_id__put": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310
return client
-price_not_greater = {
- "detail": [
- {
- "ctx": {"limit_value": 0},
- "loc": ["body", "item", "price"],
- "msg": "ensure this value is greater than 0",
- "type": "value_error.number.not_gt",
- }
- ]
-}
+@needs_py310
+def test_items_5(client: TestClient):
+ response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 5,
+ "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
+ }
@needs_py310
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/5",
- {"item": {"name": "Foo", "price": 3.0}},
- 200,
- {
- "item_id": 5,
- "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
- },
- ),
- (
- "/items/6",
- {
- "item": {
- "name": "Bar",
- "price": 0.2,
- "description": "Some bar",
- "tax": "5.4",
+def test_items_6(client: TestClient):
+ response = client.put(
+ "/items/6",
+ json={
+ "item": {
+ "name": "Bar",
+ "price": 0.2,
+ "description": "Some bar",
+ "tax": "5.4",
+ }
+ },
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 6,
+ "item": {
+ "name": "Bar",
+ "price": 0.2,
+ "description": "Some bar",
+ "tax": 5.4,
+ },
+ }
+
+
+@needs_py310
+def test_invalid_price(client: TestClient):
+ response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "greater_than",
+ "loc": ["body", "item", "price"],
+ "msg": "Input should be greater than 0",
+ "input": -3.0,
+ "ctx": {"gt": 0.0},
+ "url": match_pydantic_error_url("greater_than"),
}
- },
- 200,
- {
- "item_id": 6,
- "item": {
- "name": "Bar",
- "price": 0.2,
- "description": "Some bar",
- "tax": 5.4,
- },
- },
- ),
- ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater),
- ],
-)
-def test(path, body, expected_status, expected_response, client: TestClient):
- response = client.put(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "ctx": {"limit_value": 0},
+ "loc": ["body", "item", "price"],
+ "msg": "ensure this value is greater than 0",
+ "type": "value_error.number.not_gt",
+ }
+ ]
+ }
+ )
@needs_py310
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
- "description": {
- "title": "The description of the item",
- "maxLength": 300,
- "type": "string",
- },
+ "description": IsDict(
+ {
+ "title": "The description of the item",
+ "anyOf": [
+ {"maxLength": 300, "type": "string"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "The description of the item",
+ "maxLength": 300,
+ "type": "string",
+ }
+ ),
"price": {
"title": "Price",
"exclusiveMinimum": 0.0,
"type": "number",
"description": "The price must be greater than zero",
},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"Body_update_item_items__item_id__put": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
return client
-price_not_greater = {
- "detail": [
- {
- "ctx": {"limit_value": 0},
- "loc": ["body", "item", "price"],
- "msg": "ensure this value is greater than 0",
- "type": "value_error.number.not_gt",
- }
- ]
-}
+@needs_py39
+def test_items_5(client: TestClient):
+ response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 5,
+ "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
+ }
@needs_py39
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/5",
- {"item": {"name": "Foo", "price": 3.0}},
- 200,
- {
- "item_id": 5,
- "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
- },
- ),
- (
- "/items/6",
- {
- "item": {
- "name": "Bar",
- "price": 0.2,
- "description": "Some bar",
- "tax": "5.4",
+def test_items_6(client: TestClient):
+ response = client.put(
+ "/items/6",
+ json={
+ "item": {
+ "name": "Bar",
+ "price": 0.2,
+ "description": "Some bar",
+ "tax": "5.4",
+ }
+ },
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 6,
+ "item": {
+ "name": "Bar",
+ "price": 0.2,
+ "description": "Some bar",
+ "tax": 5.4,
+ },
+ }
+
+
+@needs_py39
+def test_invalid_price(client: TestClient):
+ response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "greater_than",
+ "loc": ["body", "item", "price"],
+ "msg": "Input should be greater than 0",
+ "input": -3.0,
+ "ctx": {"gt": 0.0},
+ "url": match_pydantic_error_url("greater_than"),
}
- },
- 200,
- {
- "item_id": 6,
- "item": {
- "name": "Bar",
- "price": 0.2,
- "description": "Some bar",
- "tax": 5.4,
- },
- },
- ),
- ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater),
- ],
-)
-def test(path, body, expected_status, expected_response, client: TestClient):
- response = client.put(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "ctx": {"limit_value": 0},
+ "loc": ["body", "item", "price"],
+ "msg": "ensure this value is greater than 0",
+ "type": "value_error.number.not_gt",
+ }
+ ]
+ }
+ )
@needs_py39
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
- "description": {
- "title": "The description of the item",
- "maxLength": 300,
- "type": "string",
- },
+ "description": IsDict(
+ {
+ "title": "The description of the item",
+ "anyOf": [
+ {"maxLength": 300, "type": "string"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "The description of the item",
+ "maxLength": 300,
+ "type": "string",
+ }
+ ),
"price": {
"title": "Price",
"exclusiveMinimum": 0.0,
"type": "number",
"description": "The price must be greater than zero",
},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"Body_update_item_items__item_id__put": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310
return client
-price_not_greater = {
- "detail": [
- {
- "ctx": {"limit_value": 0},
- "loc": ["body", "item", "price"],
- "msg": "ensure this value is greater than 0",
- "type": "value_error.number.not_gt",
- }
- ]
-}
+@needs_py310
+def test_items_5(client: TestClient):
+ response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}})
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 5,
+ "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
+ }
@needs_py310
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/5",
- {"item": {"name": "Foo", "price": 3.0}},
- 200,
- {
- "item_id": 5,
- "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None},
- },
- ),
- (
- "/items/6",
- {
- "item": {
- "name": "Bar",
- "price": 0.2,
- "description": "Some bar",
- "tax": "5.4",
+def test_items_6(client: TestClient):
+ response = client.put(
+ "/items/6",
+ json={
+ "item": {
+ "name": "Bar",
+ "price": 0.2,
+ "description": "Some bar",
+ "tax": "5.4",
+ }
+ },
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 6,
+ "item": {
+ "name": "Bar",
+ "price": 0.2,
+ "description": "Some bar",
+ "tax": 5.4,
+ },
+ }
+
+
+@needs_py310
+def test_invalid_price(client: TestClient):
+ response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "greater_than",
+ "loc": ["body", "item", "price"],
+ "msg": "Input should be greater than 0",
+ "input": -3.0,
+ "ctx": {"gt": 0.0},
+ "url": match_pydantic_error_url("greater_than"),
}
- },
- 200,
- {
- "item_id": 6,
- "item": {
- "name": "Bar",
- "price": 0.2,
- "description": "Some bar",
- "tax": 5.4,
- },
- },
- ),
- ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater),
- ],
-)
-def test(path, body, expected_status, expected_response, client: TestClient):
- response = client.put(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "ctx": {"limit_value": 0},
+ "loc": ["body", "item", "price"],
+ "msg": "ensure this value is greater than 0",
+ "type": "value_error.number.not_gt",
+ }
+ ]
+ }
+ )
@needs_py310
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
- "description": {
- "title": "The description of the item",
- "maxLength": 300,
- "type": "string",
- },
+ "description": IsDict(
+ {
+ "title": "The description of the item",
+ "anyOf": [
+ {"maxLength": 300, "type": "string"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "The description of the item",
+ "maxLength": 300,
+ "type": "string",
+ }
+ ),
"price": {
"title": "Price",
"exclusiveMinimum": 0.0,
"type": "number",
"description": "The price must be greater than zero",
},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"Body_update_item_items__item_id__put": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.body_multiple_params.tutorial001 import app
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.body_multiple_params.tutorial001 import app
+ client = TestClient(app)
+ return client
-item_id_not_int = {
- "detail": [
- {
- "loc": ["path", "item_id"],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
- }
- ]
-}
+def test_post_body_q_bar_content(client: TestClient):
+ response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5})
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 5,
+ "item": {
+ "name": "Foo",
+ "price": 50.5,
+ "description": None,
+ "tax": None,
+ },
+ "q": "bar",
+ }
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/5?q=bar",
- {"name": "Foo", "price": 50.5},
- 200,
- {
- "item_id": 5,
- "item": {
- "name": "Foo",
- "price": 50.5,
- "description": None,
- "tax": None,
- },
- "q": "bar",
- },
- ),
- ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}),
- ("/items/5", None, 200, {"item_id": 5}),
- ("/items/foo", None, 422, item_id_not_int),
- ],
-)
-def test_post_body(path, body, expected_status, expected_response):
- response = client.put(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_post_no_body_q_bar(client: TestClient):
+ response = client.put("/items/5?q=bar", json=None)
+ assert response.status_code == 200
+ assert response.json() == {"item_id": 5, "q": "bar"}
-def test_openapi_schema():
+
+def test_post_no_body(client: TestClient):
+ response = client.put("/items/5", json=None)
+ assert response.status_code == 200
+ assert response.json() == {"item_id": 5}
+
+
+def test_post_id_foo(client: TestClient):
+ response = client.put("/items/foo", json=None)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "foo",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
+
+
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
},
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
"requestBody": {
"content": {
"application/json": {
- "schema": {"$ref": "#/components/schemas/Item"}
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {"$ref": "#/components/schemas/Item"},
+ {"type": "null"},
+ ],
+ "title": "Item",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"$ref": "#/components/schemas/Item"}
+ )
}
}
},
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.body_multiple_params.tutorial001_an import app
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.body_multiple_params.tutorial001_an import app
+ client = TestClient(app)
+ return client
-item_id_not_int = {
- "detail": [
- {
- "loc": ["path", "item_id"],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
- }
- ]
-}
+def test_post_body_q_bar_content(client: TestClient):
+ response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5})
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 5,
+ "item": {
+ "name": "Foo",
+ "price": 50.5,
+ "description": None,
+ "tax": None,
+ },
+ "q": "bar",
+ }
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/5?q=bar",
- {"name": "Foo", "price": 50.5},
- 200,
- {
- "item_id": 5,
- "item": {
- "name": "Foo",
- "price": 50.5,
- "description": None,
- "tax": None,
- },
- "q": "bar",
- },
- ),
- ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}),
- ("/items/5", None, 200, {"item_id": 5}),
- ("/items/foo", None, 422, item_id_not_int),
- ],
-)
-def test_post_body(path, body, expected_status, expected_response):
- response = client.put(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_post_no_body_q_bar(client: TestClient):
+ response = client.put("/items/5?q=bar", json=None)
+ assert response.status_code == 200
+ assert response.json() == {"item_id": 5, "q": "bar"}
-def test_openapi_schema():
+
+def test_post_no_body(client: TestClient):
+ response = client.put("/items/5", json=None)
+ assert response.status_code == 200
+ assert response.json() == {"item_id": 5}
+
+
+def test_post_id_foo(client: TestClient):
+ response = client.put("/items/foo", json=None)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "foo",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
+
+
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
},
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
"requestBody": {
"content": {
"application/json": {
- "schema": {"$ref": "#/components/schemas/Item"}
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {"$ref": "#/components/schemas/Item"},
+ {"type": "null"},
+ ],
+ "title": "Item",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"$ref": "#/components/schemas/Item"}
+ )
}
}
},
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310
return client
-item_id_not_int = {
- "detail": [
- {
- "loc": ["path", "item_id"],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
- }
- ]
-}
+@needs_py310
+def test_post_body_q_bar_content(client: TestClient):
+ response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5})
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 5,
+ "item": {
+ "name": "Foo",
+ "price": 50.5,
+ "description": None,
+ "tax": None,
+ },
+ "q": "bar",
+ }
@needs_py310
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/5?q=bar",
- {"name": "Foo", "price": 50.5},
- 200,
- {
- "item_id": 5,
- "item": {
- "name": "Foo",
- "price": 50.5,
- "description": None,
- "tax": None,
- },
- "q": "bar",
- },
- ),
- ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}),
- ("/items/5", None, 200, {"item_id": 5}),
- ("/items/foo", None, 422, item_id_not_int),
- ],
-)
-def test_post_body(path, body, expected_status, expected_response, client: TestClient):
- response = client.put(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_post_no_body_q_bar(client: TestClient):
+ response = client.put("/items/5?q=bar", json=None)
+ assert response.status_code == 200
+ assert response.json() == {"item_id": 5, "q": "bar"}
+
+
+@needs_py310
+def test_post_no_body(client: TestClient):
+ response = client.put("/items/5", json=None)
+ assert response.status_code == 200
+ assert response.json() == {"item_id": 5}
+
+
+@needs_py310
+def test_post_id_foo(client: TestClient):
+ response = client.put("/items/foo", json=None)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "foo",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
@needs_py310
},
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
"requestBody": {
"content": {
"application/json": {
- "schema": {"$ref": "#/components/schemas/Item"}
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {"$ref": "#/components/schemas/Item"},
+ {"type": "null"},
+ ],
+ "title": "Item",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"$ref": "#/components/schemas/Item"}
+ )
}
}
},
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
return client
-item_id_not_int = {
- "detail": [
- {
- "loc": ["path", "item_id"],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
- }
- ]
-}
+@needs_py39
+def test_post_body_q_bar_content(client: TestClient):
+ response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5})
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 5,
+ "item": {
+ "name": "Foo",
+ "price": 50.5,
+ "description": None,
+ "tax": None,
+ },
+ "q": "bar",
+ }
@needs_py39
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/5?q=bar",
- {"name": "Foo", "price": 50.5},
- 200,
- {
- "item_id": 5,
- "item": {
- "name": "Foo",
- "price": 50.5,
- "description": None,
- "tax": None,
- },
- "q": "bar",
- },
- ),
- ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}),
- ("/items/5", None, 200, {"item_id": 5}),
- ("/items/foo", None, 422, item_id_not_int),
- ],
-)
-def test_post_body(path, body, expected_status, expected_response, client: TestClient):
- response = client.put(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_post_no_body_q_bar(client: TestClient):
+ response = client.put("/items/5?q=bar", json=None)
+ assert response.status_code == 200
+ assert response.json() == {"item_id": 5, "q": "bar"}
+
+
+@needs_py39
+def test_post_no_body(client: TestClient):
+ response = client.put("/items/5", json=None)
+ assert response.status_code == 200
+ assert response.json() == {"item_id": 5}
+
+
+@needs_py39
+def test_post_id_foo(client: TestClient):
+ response = client.put("/items/foo", json=None)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "foo",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
@needs_py39
},
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
"requestBody": {
"content": {
"application/json": {
- "schema": {"$ref": "#/components/schemas/Item"}
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {"$ref": "#/components/schemas/Item"},
+ {"type": "null"},
+ ],
+ "title": "Item",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"$ref": "#/components/schemas/Item"}
+ )
}
}
},
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310
return client
-item_id_not_int = {
- "detail": [
- {
- "loc": ["path", "item_id"],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
- }
- ]
-}
+@needs_py310
+def test_post_body_q_bar_content(client: TestClient):
+ response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5})
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 5,
+ "item": {
+ "name": "Foo",
+ "price": 50.5,
+ "description": None,
+ "tax": None,
+ },
+ "q": "bar",
+ }
@needs_py310
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/5?q=bar",
- {"name": "Foo", "price": 50.5},
- 200,
- {
- "item_id": 5,
- "item": {
- "name": "Foo",
- "price": 50.5,
- "description": None,
- "tax": None,
- },
- "q": "bar",
- },
- ),
- ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}),
- ("/items/5", None, 200, {"item_id": 5}),
- ("/items/foo", None, 422, item_id_not_int),
- ],
-)
-def test_post_body(path, body, expected_status, expected_response, client: TestClient):
- response = client.put(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_post_no_body_q_bar(client: TestClient):
+ response = client.put("/items/5?q=bar", json=None)
+ assert response.status_code == 200
+ assert response.json() == {"item_id": 5, "q": "bar"}
+
+
+@needs_py310
+def test_post_no_body(client: TestClient):
+ response = client.put("/items/5", json=None)
+ assert response.status_code == 200
+ assert response.json() == {"item_id": 5}
+
+
+@needs_py310
+def test_post_id_foo(client: TestClient):
+ response = client.put("/items/foo", json=None)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "foo",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
@needs_py310
},
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
"requestBody": {
"content": {
"application/json": {
- "schema": {"$ref": "#/components/schemas/Item"}
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {"$ref": "#/components/schemas/Item"},
+ {"type": "null"},
+ ],
+ "title": "Item",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"$ref": "#/components/schemas/Item"}
+ )
}
}
},
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.body_multiple_params.tutorial003 import app
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.body_multiple_params.tutorial003 import app
+ client = TestClient(app)
+ return client
-# Test required and embedded body parameters with no bodies sent
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/5",
- {
- "importance": 2,
- "item": {"name": "Foo", "price": 50.5},
- "user": {"username": "Dave"},
- },
- 200,
- {
- "item_id": 5,
- "importance": 2,
- "item": {
- "name": "Foo",
- "price": 50.5,
- "description": None,
- "tax": None,
- },
- "user": {"username": "Dave", "full_name": None},
- },
- ),
- (
- "/items/5",
- None,
- 422,
- {
- "detail": [
- {
- "loc": ["body", "item"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "user"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "importance"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- },
- ),
- (
- "/items/5",
- [],
- 422,
- {
- "detail": [
- {
- "loc": ["body", "item"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "user"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "importance"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- },
- ),
- ],
-)
-def test_post_body(path, body, expected_status, expected_response):
- response = client.put(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+
+def test_post_body_valid(client: TestClient):
+ response = client.put(
+ "/items/5",
+ json={
+ "importance": 2,
+ "item": {"name": "Foo", "price": 50.5},
+ "user": {"username": "Dave"},
+ },
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 5,
+ "importance": 2,
+ "item": {
+ "name": "Foo",
+ "price": 50.5,
+ "description": None,
+ "tax": None,
+ },
+ "user": {"username": "Dave", "full_name": None},
+ }
+
+
+def test_post_body_no_data(client: TestClient):
+ response = client.put("/items/5", json=None)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "item"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "user"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "importance"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "item"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "user"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "importance"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-def test_openapi_schema():
+def test_post_body_empty_list(client: TestClient):
+ response = client.put("/items/5", json=[])
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "item"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "user"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "importance"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "item"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "user"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "importance"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"User": {
"type": "object",
"properties": {
"username": {"title": "Username", "type": "string"},
- "full_name": {"title": "Full Name", "type": "string"},
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
},
},
"Body_update_item_items__item_id__put": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.body_multiple_params.tutorial003_an import app
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.body_multiple_params.tutorial003_an import app
+ client = TestClient(app)
+ return client
-# Test required and embedded body parameters with no bodies sent
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/5",
- {
- "importance": 2,
- "item": {"name": "Foo", "price": 50.5},
- "user": {"username": "Dave"},
- },
- 200,
- {
- "item_id": 5,
- "importance": 2,
- "item": {
- "name": "Foo",
- "price": 50.5,
- "description": None,
- "tax": None,
- },
- "user": {"username": "Dave", "full_name": None},
- },
- ),
- (
- "/items/5",
- None,
- 422,
- {
- "detail": [
- {
- "loc": ["body", "item"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "user"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "importance"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- },
- ),
- (
- "/items/5",
- [],
- 422,
- {
- "detail": [
- {
- "loc": ["body", "item"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "user"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "importance"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- },
- ),
- ],
-)
-def test_post_body(path, body, expected_status, expected_response):
- response = client.put(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+
+def test_post_body_valid(client: TestClient):
+ response = client.put(
+ "/items/5",
+ json={
+ "importance": 2,
+ "item": {"name": "Foo", "price": 50.5},
+ "user": {"username": "Dave"},
+ },
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 5,
+ "importance": 2,
+ "item": {
+ "name": "Foo",
+ "price": 50.5,
+ "description": None,
+ "tax": None,
+ },
+ "user": {"username": "Dave", "full_name": None},
+ }
+
+
+def test_post_body_no_data(client: TestClient):
+ response = client.put("/items/5", json=None)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "item"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "user"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "importance"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "item"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "user"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "importance"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-def test_openapi_schema():
+def test_post_body_empty_list(client: TestClient):
+ response = client.put("/items/5", json=[])
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "item"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "user"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "importance"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "item"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "user"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "importance"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"User": {
"type": "object",
"properties": {
"username": {"title": "Username", "type": "string"},
- "full_name": {"title": "Full Name", "type": "string"},
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
},
},
"Body_update_item_items__item_id__put": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310
return client
-# Test required and embedded body parameters with no bodies sent
@needs_py310
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/5",
- {
- "importance": 2,
- "item": {"name": "Foo", "price": 50.5},
- "user": {"username": "Dave"},
- },
- 200,
- {
- "item_id": 5,
- "importance": 2,
- "item": {
- "name": "Foo",
- "price": 50.5,
- "description": None,
- "tax": None,
- },
- "user": {"username": "Dave", "full_name": None},
- },
- ),
- (
- "/items/5",
- None,
- 422,
- {
- "detail": [
- {
- "loc": ["body", "item"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "user"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "importance"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- },
- ),
- (
- "/items/5",
- [],
- 422,
- {
- "detail": [
- {
- "loc": ["body", "item"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "user"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "importance"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- },
- ),
- ],
-)
-def test_post_body(path, body, expected_status, expected_response, client: TestClient):
- response = client.put(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_post_body_valid(client: TestClient):
+ response = client.put(
+ "/items/5",
+ json={
+ "importance": 2,
+ "item": {"name": "Foo", "price": 50.5},
+ "user": {"username": "Dave"},
+ },
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 5,
+ "importance": 2,
+ "item": {
+ "name": "Foo",
+ "price": 50.5,
+ "description": None,
+ "tax": None,
+ },
+ "user": {"username": "Dave", "full_name": None},
+ }
+
+
+@needs_py310
+def test_post_body_no_data(client: TestClient):
+ response = client.put("/items/5", json=None)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "item"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "user"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "importance"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "item"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "user"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "importance"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+@needs_py310
+def test_post_body_empty_list(client: TestClient):
+ response = client.put("/items/5", json=[])
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "item"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "user"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "importance"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "item"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "user"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "importance"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
@needs_py310
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"User": {
"type": "object",
"properties": {
"username": {"title": "Username", "type": "string"},
- "full_name": {"title": "Full Name", "type": "string"},
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
},
},
"Body_update_item_items__item_id__put": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
return client
-# Test required and embedded body parameters with no bodies sent
@needs_py39
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/5",
- {
- "importance": 2,
- "item": {"name": "Foo", "price": 50.5},
- "user": {"username": "Dave"},
- },
- 200,
- {
- "item_id": 5,
- "importance": 2,
- "item": {
- "name": "Foo",
- "price": 50.5,
- "description": None,
- "tax": None,
- },
- "user": {"username": "Dave", "full_name": None},
- },
- ),
- (
- "/items/5",
- None,
- 422,
- {
- "detail": [
- {
- "loc": ["body", "item"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "user"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "importance"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- },
- ),
- (
- "/items/5",
- [],
- 422,
- {
- "detail": [
- {
- "loc": ["body", "item"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "user"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "importance"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- },
- ),
- ],
-)
-def test_post_body(path, body, expected_status, expected_response, client: TestClient):
- response = client.put(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_post_body_valid(client: TestClient):
+ response = client.put(
+ "/items/5",
+ json={
+ "importance": 2,
+ "item": {"name": "Foo", "price": 50.5},
+ "user": {"username": "Dave"},
+ },
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 5,
+ "importance": 2,
+ "item": {
+ "name": "Foo",
+ "price": 50.5,
+ "description": None,
+ "tax": None,
+ },
+ "user": {"username": "Dave", "full_name": None},
+ }
+
+
+@needs_py39
+def test_post_body_no_data(client: TestClient):
+ response = client.put("/items/5", json=None)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "item"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "user"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "importance"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "item"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "user"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "importance"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+@needs_py39
+def test_post_body_empty_list(client: TestClient):
+ response = client.put("/items/5", json=[])
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "item"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "user"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "importance"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "item"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "user"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "importance"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
@needs_py39
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"User": {
"type": "object",
"properties": {
"username": {"title": "Username", "type": "string"},
- "full_name": {"title": "Full Name", "type": "string"},
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
},
},
"Body_update_item_items__item_id__put": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310
return client
-# Test required and embedded body parameters with no bodies sent
@needs_py310
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/items/5",
- {
- "importance": 2,
- "item": {"name": "Foo", "price": 50.5},
- "user": {"username": "Dave"},
- },
- 200,
- {
- "item_id": 5,
- "importance": 2,
- "item": {
- "name": "Foo",
- "price": 50.5,
- "description": None,
- "tax": None,
- },
- "user": {"username": "Dave", "full_name": None},
- },
- ),
- (
- "/items/5",
- None,
- 422,
- {
- "detail": [
- {
- "loc": ["body", "item"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "user"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "importance"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- },
- ),
- (
- "/items/5",
- [],
- 422,
- {
- "detail": [
- {
- "loc": ["body", "item"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "user"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "importance"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- },
- ),
- ],
-)
-def test_post_body(path, body, expected_status, expected_response, client: TestClient):
- response = client.put(path, json=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_post_body_valid(client: TestClient):
+ response = client.put(
+ "/items/5",
+ json={
+ "importance": 2,
+ "item": {"name": "Foo", "price": 50.5},
+ "user": {"username": "Dave"},
+ },
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": 5,
+ "importance": 2,
+ "item": {
+ "name": "Foo",
+ "price": 50.5,
+ "description": None,
+ "tax": None,
+ },
+ "user": {"username": "Dave", "full_name": None},
+ }
+
+
+@needs_py310
+def test_post_body_no_data(client: TestClient):
+ response = client.put("/items/5", json=None)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "item"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "user"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "importance"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "item"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "user"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "importance"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
+
+
+@needs_py310
+def test_post_body_empty_list(client: TestClient):
+ response = client.put("/items/5", json=[])
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "item"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "user"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "importance"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "item"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "user"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "importance"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
@needs_py310
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"User": {
"type": "object",
"properties": {
"username": {"title": "Username", "type": "string"},
- "full_name": {"title": "Full Name", "type": "string"},
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
},
},
"Body_update_item_items__item_id__put": {
+import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.body_nested_models.tutorial009 import app
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.body_nested_models.tutorial009 import app
+ client = TestClient(app)
+ return client
-def test_post_body():
+
+def test_post_body(client: TestClient):
data = {"2": 2.2, "3": 3.3}
response = client.post("/index-weights/", json=data)
assert response.status_code == 200, response.text
assert response.json() == data
-def test_post_invalid_body():
+def test_post_invalid_body(client: TestClient):
data = {"foo": 2.2, "3": 3.3}
response = client.post("/index-weights/", json=data)
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["body", "__key__"],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
- }
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["body", "foo", "[key]"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "foo",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "__key__"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
data = {"foo": 2.2, "3": 3.3}
response = client.post("/index-weights/", json=data)
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["body", "__key__"],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
- }
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["body", "foo", "[key]"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "foo",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "__key__"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
@needs_py39
+import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
-from docs_src.body_updates.tutorial001 import app
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.body_updates.tutorial001 import app
+ client = TestClient(app)
+ return client
-def test_get():
+
+def test_get(client: TestClient):
response = client.get("/items/baz")
assert response.status_code == 200, response.text
assert response.json() == {
}
-def test_put():
+def test_put(client: TestClient):
response = client.put(
"/items/bar", json={"name": "Barz", "price": 3, "description": None}
)
}
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"title": "Item",
"type": "object",
"properties": {
- "name": {"title": "Name", "type": "string"},
- "description": {"title": "Description", "type": "string"},
- "price": {"title": "Price", "type": "number"},
+ "name": IsDict(
+ {
+ "title": "Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Name", "type": "string"}
+ ),
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
+ "price": IsDict(
+ {
+ "title": "Price",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Price", "type": "number"}
+ ),
"tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": {
"title": "Tags",
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"title": "Item",
"type": "object",
"properties": {
- "name": {"title": "Name", "type": "string"},
- "description": {"title": "Description", "type": "string"},
- "price": {"title": "Price", "type": "number"},
+ "name": IsDict(
+ {
+ "title": "Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Name", "type": "string"}
+ ),
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
+ "price": IsDict(
+ {
+ "title": "Price",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Price", "type": "number"}
+ ),
"tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": {
"title": "Tags",
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
"title": "Item",
"type": "object",
"properties": {
- "name": {"title": "Name", "type": "string"},
- "description": {"title": "Description", "type": "string"},
- "price": {"title": "Price", "type": "number"},
+ "name": IsDict(
+ {
+ "title": "Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Name", "type": "string"}
+ ),
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
+ "price": IsDict(
+ {
+ "title": "Price",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Price", "type": "number"}
+ ),
"tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": {
"title": "Tags",
from fastapi.testclient import TestClient
-from docs_src.conditional_openapi import tutorial001
+from ...utils import needs_pydanticv2
-def test_disable_openapi(monkeypatch):
- monkeypatch.setenv("OPENAPI_URL", "")
+def get_client() -> TestClient:
+ from docs_src.conditional_openapi import tutorial001
+
importlib.reload(tutorial001)
+
client = TestClient(tutorial001.app)
+ return client
+
+
+@needs_pydanticv2
+def test_disable_openapi(monkeypatch):
+ monkeypatch.setenv("OPENAPI_URL", "")
+ # Load the client after setting the env var
+ client = get_client()
response = client.get("/openapi.json")
assert response.status_code == 404, response.text
response = client.get("/docs")
assert response.status_code == 404, response.text
+@needs_pydanticv2
def test_root():
- client = TestClient(tutorial001.app)
+ client = get_client()
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
+@needs_pydanticv2
def test_default_openapi():
- importlib.reload(tutorial001)
- client = TestClient(tutorial001.app)
+ client = get_client()
response = client.get("/docs")
assert response.status_code == 200, response.text
response = client.get("/redoc")
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.cookie_params.tutorial001 import app
"parameters": [
{
"required": False,
- "schema": {"title": "Ads Id", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Ads Id",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Ads Id", "type": "string"}
+ ),
"name": "ads_id",
"in": "cookie",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.cookie_params.tutorial001_an import app
"parameters": [
{
"required": False,
- "schema": {"title": "Ads Id", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Ads Id",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Ads Id", "type": "string"}
+ ),
"name": "ads_id",
"in": "cookie",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"parameters": [
{
"required": False,
- "schema": {"title": "Ads Id", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Ads Id",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Ads Id", "type": "string"}
+ ),
"name": "ads_id",
"in": "cookie",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
"parameters": [
{
"required": False,
- "schema": {"title": "Ads Id", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Ads Id",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Ads Id", "type": "string"}
+ ),
"name": "ads_id",
"in": "cookie",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"parameters": [
{
"required": False,
- "schema": {"title": "Ads Id", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Ads Id",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Ads Id", "type": "string"}
+ ),
"name": "ads_id",
"in": "cookie",
}
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from docs_src.custom_request_and_route.tutorial002 import app
def test_exception_handler_body_access():
response = client.post("/", json={"numbers": [1, 2, 3]})
-
- assert response.json() == {
- "detail": {
- "body": '{"numbers": [1, 2, 3]}',
- "errors": [
- {
- "loc": ["body"],
- "msg": "value is not a valid list",
- "type": "type_error.list",
- }
- ],
+ assert response.json() == IsDict(
+ {
+ "detail": {
+ "errors": [
+ {
+ "type": "list_type",
+ "loc": ["body"],
+ "msg": "Input should be a valid list",
+ "input": {"numbers": [1, 2, 3]},
+ "url": match_pydantic_error_url("list_type"),
+ }
+ ],
+ "body": '{"numbers": [1, 2, 3]}',
+ }
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": {
+ "body": '{"numbers": [1, 2, 3]}',
+ "errors": [
+ {
+ "loc": ["body"],
+ "msg": "value is not a valid list",
+ "type": "type_error.list",
+ }
+ ],
+ }
}
- }
+ )
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from docs_src.dataclasses.tutorial001 import app
def test_post_invalid_item():
response = client.post("/items/", json={"name": "Foo", "price": "invalid price"})
assert response.status_code == 422
- assert response.json() == {
- "detail": [
- {
- "loc": ["body", "price"],
- "msg": "value is not a valid float",
- "type": "type_error.float",
- }
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "float_parsing",
+ "loc": ["body", "price"],
+ "msg": "Input should be a valid number, unable to parse string as a number",
+ "input": "invalid price",
+ "url": match_pydantic_error_url("float_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "price"],
+ "msg": "value is not a valid float",
+ "type": "type_error.float",
+ }
+ ]
+ }
+ )
def test_openapi_schema():
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"ValidationError": {
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.dataclasses.tutorial002 import app
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
- "tags": {
- "title": "Tags",
- "type": "array",
- "items": {"type": "string"},
- },
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "tags": IsDict(
+ {
+ "title": "Tags",
+ "type": "array",
+ "items": {"type": "string"},
+ "default": [],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Tags",
+ "type": "array",
+ "items": {"type": "string"},
+ }
+ ),
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
}
}
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.dataclasses.tutorial003 import app
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
- "items": {
- "title": "Items",
- "type": "array",
- "items": {"$ref": "#/components/schemas/Item"},
- },
+ "items": IsDict(
+ {
+ "title": "Items",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ "default": [],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Items",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ ),
},
},
"HTTPValidationError": {
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.dependencies.tutorial001 import app
"parameters": [
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
"parameters": [
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.dependencies.tutorial001_an import app
"parameters": [
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
"parameters": [
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"parameters": [
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
"parameters": [
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
"parameters": [
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
"parameters": [
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"parameters": [
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
"parameters": [
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.dependencies.tutorial004 import app
"parameters": [
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.dependencies.tutorial004_an import app
"parameters": [
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"parameters": [
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
"parameters": [
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"parameters": [
{
"required": False,
- "schema": {"title": "Q", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Q", "type": "string"}
+ ),
"name": "q",
"in": "query",
},
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from docs_src.dependencies.tutorial006 import app
def test_get_no_headers():
response = client.get("/items/")
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["header", "x-key"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["header", "x-key"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["header", "x-key"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
def test_get_invalid_one_header():
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from docs_src.dependencies.tutorial006_an import app
def test_get_no_headers():
response = client.get("/items/")
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["header", "x-key"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["header", "x-key"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["header", "x-key"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
def test_get_invalid_one_header():
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
def test_get_no_headers(client: TestClient):
response = client.get("/items/")
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["header", "x-key"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["header", "x-key"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["header", "x-key"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
@needs_py39
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from docs_src.dependencies.tutorial012 import app
def test_get_no_headers_items():
response = client.get("/items/")
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["header", "x-key"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["header", "x-key"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["header", "x-key"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
def test_get_no_headers_users():
response = client.get("/users/")
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["header", "x-key"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["header", "x-key"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["header", "x-key"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
def test_get_invalid_one_header_items():
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from docs_src.dependencies.tutorial012_an import app
def test_get_no_headers_items():
response = client.get("/items/")
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["header", "x-key"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["header", "x-key"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["header", "x-key"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
def test_get_no_headers_users():
response = client.get("/users/")
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["header", "x-key"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["header", "x-key"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["header", "x-key"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
def test_get_invalid_one_header_items():
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
def test_get_no_headers_items(client: TestClient):
response = client.get("/items/")
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["header", "x-key"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["header", "x-key"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["header", "x-key"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
@needs_py39
def test_get_no_headers_users(client: TestClient):
response = client.get("/users/")
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["header", "x-token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["header", "x-key"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "x-token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["header", "x-key"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "x-token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["header", "x-key"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
@needs_py39
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.extra_data_types.tutorial001 import app
"requestBody": {
"content": {
"application/json": {
- "schema": {
- "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
- }
+ "schema": IsDict(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+ }
+ ],
+ "title": "Body",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+ }
+ )
}
}
},
"title": "Body_read_items_items__item_id__put",
"type": "object",
"properties": {
- "start_datetime": {
- "title": "Start Datetime",
- "type": "string",
- "format": "date-time",
- },
- "end_datetime": {
- "title": "End Datetime",
- "type": "string",
- "format": "date-time",
- },
- "repeat_at": {
- "title": "Repeat At",
- "type": "string",
- "format": "time",
- },
- "process_after": {
- "title": "Process After",
- "type": "number",
- "format": "time-delta",
- },
+ "start_datetime": IsDict(
+ {
+ "title": "Start Datetime",
+ "anyOf": [
+ {"type": "string", "format": "date-time"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Start Datetime",
+ "type": "string",
+ "format": "date-time",
+ }
+ ),
+ "end_datetime": IsDict(
+ {
+ "title": "End Datetime",
+ "anyOf": [
+ {"type": "string", "format": "date-time"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "End Datetime",
+ "type": "string",
+ "format": "date-time",
+ }
+ ),
+ "repeat_at": IsDict(
+ {
+ "title": "Repeat At",
+ "anyOf": [
+ {"type": "string", "format": "time"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Repeat At",
+ "type": "string",
+ "format": "time",
+ }
+ ),
+ "process_after": IsDict(
+ {
+ "title": "Process After",
+ "anyOf": [
+ {"type": "string", "format": "duration"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Process After",
+ "type": "number",
+ "format": "time-delta",
+ }
+ ),
},
},
"ValidationError": {
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.extra_data_types.tutorial001_an import app
"requestBody": {
"content": {
"application/json": {
- "schema": {
- "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
- }
+ "schema": IsDict(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+ }
+ ],
+ "title": "Body",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+ }
+ )
}
}
},
"title": "Body_read_items_items__item_id__put",
"type": "object",
"properties": {
- "start_datetime": {
- "title": "Start Datetime",
- "type": "string",
- "format": "date-time",
- },
- "end_datetime": {
- "title": "End Datetime",
- "type": "string",
- "format": "date-time",
- },
- "repeat_at": {
- "title": "Repeat At",
- "type": "string",
- "format": "time",
- },
- "process_after": {
- "title": "Process After",
- "type": "number",
- "format": "time-delta",
- },
+ "start_datetime": IsDict(
+ {
+ "title": "Start Datetime",
+ "anyOf": [
+ {"type": "string", "format": "date-time"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Start Datetime",
+ "type": "string",
+ "format": "date-time",
+ }
+ ),
+ "end_datetime": IsDict(
+ {
+ "title": "End Datetime",
+ "anyOf": [
+ {"type": "string", "format": "date-time"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "End Datetime",
+ "type": "string",
+ "format": "date-time",
+ }
+ ),
+ "repeat_at": IsDict(
+ {
+ "title": "Repeat At",
+ "anyOf": [
+ {"type": "string", "format": "time"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Repeat At",
+ "type": "string",
+ "format": "time",
+ }
+ ),
+ "process_after": IsDict(
+ {
+ "title": "Process After",
+ "anyOf": [
+ {"type": "string", "format": "duration"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Process After",
+ "type": "number",
+ "format": "time-delta",
+ }
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"requestBody": {
"content": {
"application/json": {
- "schema": {
- "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
- }
+ "schema": IsDict(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+ }
+ ],
+ "title": "Body",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+ }
+ )
}
}
},
"title": "Body_read_items_items__item_id__put",
"type": "object",
"properties": {
- "start_datetime": {
- "title": "Start Datetime",
- "type": "string",
- "format": "date-time",
- },
- "end_datetime": {
- "title": "End Datetime",
- "type": "string",
- "format": "date-time",
- },
- "repeat_at": {
- "title": "Repeat At",
- "type": "string",
- "format": "time",
- },
- "process_after": {
- "title": "Process After",
- "type": "number",
- "format": "time-delta",
- },
+ "start_datetime": IsDict(
+ {
+ "title": "Start Datetime",
+ "anyOf": [
+ {"type": "string", "format": "date-time"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Start Datetime",
+ "type": "string",
+ "format": "date-time",
+ }
+ ),
+ "end_datetime": IsDict(
+ {
+ "title": "End Datetime",
+ "anyOf": [
+ {"type": "string", "format": "date-time"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "End Datetime",
+ "type": "string",
+ "format": "date-time",
+ }
+ ),
+ "repeat_at": IsDict(
+ {
+ "title": "Repeat At",
+ "anyOf": [
+ {"type": "string", "format": "time"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Repeat At",
+ "type": "string",
+ "format": "time",
+ }
+ ),
+ "process_after": IsDict(
+ {
+ "title": "Process After",
+ "anyOf": [
+ {"type": "string", "format": "duration"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Process After",
+ "type": "number",
+ "format": "time-delta",
+ }
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
"requestBody": {
"content": {
"application/json": {
- "schema": {
- "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
- }
+ "schema": IsDict(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+ }
+ ],
+ "title": "Body",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+ }
+ )
}
}
},
"title": "Body_read_items_items__item_id__put",
"type": "object",
"properties": {
- "start_datetime": {
- "title": "Start Datetime",
- "type": "string",
- "format": "date-time",
- },
- "end_datetime": {
- "title": "End Datetime",
- "type": "string",
- "format": "date-time",
- },
- "repeat_at": {
- "title": "Repeat At",
- "type": "string",
- "format": "time",
- },
- "process_after": {
- "title": "Process After",
- "type": "number",
- "format": "time-delta",
- },
+ "start_datetime": IsDict(
+ {
+ "title": "Start Datetime",
+ "anyOf": [
+ {"type": "string", "format": "date-time"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Start Datetime",
+ "type": "string",
+ "format": "date-time",
+ }
+ ),
+ "end_datetime": IsDict(
+ {
+ "title": "End Datetime",
+ "anyOf": [
+ {"type": "string", "format": "date-time"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "End Datetime",
+ "type": "string",
+ "format": "date-time",
+ }
+ ),
+ "repeat_at": IsDict(
+ {
+ "title": "Repeat At",
+ "anyOf": [
+ {"type": "string", "format": "time"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Repeat At",
+ "type": "string",
+ "format": "time",
+ }
+ ),
+ "process_after": IsDict(
+ {
+ "title": "Process After",
+ "anyOf": [
+ {"type": "string", "format": "duration"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Process After",
+ "type": "number",
+ "format": "time-delta",
+ }
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"requestBody": {
"content": {
"application/json": {
- "schema": {
- "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
- }
+ "schema": IsDict(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+ }
+ ],
+ "title": "Body",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+ }
+ )
}
}
},
"title": "Body_read_items_items__item_id__put",
"type": "object",
"properties": {
- "start_datetime": {
- "title": "Start Datetime",
- "type": "string",
- "format": "date-time",
- },
- "end_datetime": {
- "title": "End Datetime",
- "type": "string",
- "format": "date-time",
- },
- "repeat_at": {
- "title": "Repeat At",
- "type": "string",
- "format": "time",
- },
- "process_after": {
- "title": "Process After",
- "type": "number",
- "format": "time-delta",
- },
+ "start_datetime": IsDict(
+ {
+ "title": "Start Datetime",
+ "anyOf": [
+ {"type": "string", "format": "date-time"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Start Datetime",
+ "type": "string",
+ "format": "date-time",
+ }
+ ),
+ "end_datetime": IsDict(
+ {
+ "title": "End Datetime",
+ "anyOf": [
+ {"type": "string", "format": "date-time"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "End Datetime",
+ "type": "string",
+ "format": "date-time",
+ }
+ ),
+ "repeat_at": IsDict(
+ {
+ "title": "Repeat At",
+ "anyOf": [
+ {"type": "string", "format": "time"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Repeat At",
+ "type": "string",
+ "format": "time",
+ }
+ ),
+ "process_after": IsDict(
+ {
+ "title": "Process After",
+ "anyOf": [
+ {"type": "string", "format": "duration"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Process After",
+ "type": "number",
+ "format": "time-delta",
+ }
+ ),
},
},
"ValidationError": {
def test_get_validation_error():
response = client.get("/items/foo")
assert response.status_code == 400, response.text
- validation_error_str_lines = [
- b"1 validation error for Request",
- b"path -> item_id",
- b" value is not a valid integer (type=type_error.integer)",
- ]
- assert response.content == b"\n".join(validation_error_str_lines)
+ # TODO: remove when deprecating Pydantic v1
+ assert (
+ # TODO: remove when deprecating Pydantic v1
+ "path -> item_id" in response.text
+ or "'loc': ('path', 'item_id')" in response.text
+ )
+ assert (
+ # TODO: remove when deprecating Pydantic v1
+ "value is not a valid integer" in response.text
+ or "Input should be a valid integer, unable to parse string as an integer"
+ in response.text
+ )
def test_get_http_error():
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from docs_src.handling_errors.tutorial005 import app
def test_post_validation_error():
response = client.post("/items/", json={"title": "towel", "size": "XL"})
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["body", "size"],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
- }
- ],
- "body": {"title": "towel", "size": "XL"},
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["body", "size"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "XL",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ],
+ "body": {"title": "towel", "size": "XL"},
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "size"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ],
+ "body": {"title": "towel", "size": "XL"},
+ }
+ )
def test_post():
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from docs_src.handling_errors.tutorial006 import app
def test_get_validation_error():
response = client.get("/items/foo")
assert response.status_code == 422, response.text
- assert response.json() == {
- "detail": [
- {
- "loc": ["path", "item_id"],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
- }
- ]
- }
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "int_parsing",
+ "loc": ["path", "item_id"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "foo",
+ "url": match_pydantic_error_url("int_parsing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["path", "item_id"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ }
+ ]
+ }
+ )
def test_get_http_error():
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.header_params.tutorial001 import app
assert response.json() == expected_response
-def test_openapi():
+def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == {
"parameters": [
{
"required": False,
- "schema": {"title": "User-Agent", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "User-Agent",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "User-Agent", "type": "string"}
+ ),
"name": "user-agent",
"in": "header",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.header_params.tutorial001_an import app
"parameters": [
{
"required": False,
- "schema": {"title": "User-Agent", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "User-Agent",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "User-Agent", "type": "string"}
+ ),
"name": "user-agent",
"in": "header",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"parameters": [
{
"required": False,
- "schema": {"title": "User-Agent", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "User-Agent",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "User-Agent", "type": "string"}
+ ),
"name": "user-agent",
"in": "header",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"parameters": [
{
"required": False,
- "schema": {"title": "User-Agent", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "User-Agent",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "User-Agent", "type": "string"}
+ ),
"name": "user-agent",
"in": "header",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.header_params.tutorial002 import app
"parameters": [
{
"required": False,
- "schema": {"title": "Strange Header", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Strange Header",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Strange Header", "type": "string"}
+ ),
"name": "strange_header",
"in": "header",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.header_params.tutorial002_an import app
"parameters": [
{
"required": False,
- "schema": {"title": "Strange Header", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Strange Header",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Strange Header", "type": "string"}
+ ),
"name": "strange_header",
"in": "header",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"parameters": [
{
"required": False,
- "schema": {"title": "Strange Header", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Strange Header",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Strange Header", "type": "string"}
+ ),
"name": "strange_header",
"in": "header",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
"parameters": [
{
"required": False,
- "schema": {"title": "Strange Header", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Strange Header",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Strange Header", "type": "string"}
+ ),
"name": "strange_header",
"in": "header",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"parameters": [
{
"required": False,
- "schema": {"title": "Strange Header", "type": "string"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Strange Header",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Strange Header", "type": "string"}
+ ),
"name": "strange_header",
"in": "header",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.header_params.tutorial003 import app
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
- # insert_assert(response.json())
assert response.json() == {
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"parameters": [
{
"required": False,
- "schema": {
- "title": "X-Token",
- "type": "array",
- "items": {"type": "string"},
- },
+ "schema": IsDict(
+ {
+ "title": "X-Token",
+ "anyOf": [
+ {"type": "array", "items": {"type": "string"}},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "X-Token",
+ "type": "array",
+ "items": {"type": "string"},
+ }
+ ),
"name": "x-token",
"in": "header",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.header_params.tutorial003_an import app
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
- # insert_assert(response.json())
assert response.json() == {
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"parameters": [
{
"required": False,
- "schema": {
- "title": "X-Token",
- "type": "array",
- "items": {"type": "string"},
- },
+ "schema": IsDict(
+ {
+ "title": "X-Token",
+ "anyOf": [
+ {"type": "array", "items": {"type": "string"}},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "X-Token",
+ "type": "array",
+ "items": {"type": "string"},
+ }
+ ),
"name": "x-token",
"in": "header",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200
- # insert_assert(response.json())
assert response.json() == {
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"parameters": [
{
"required": False,
- "schema": {
- "title": "X-Token",
- "type": "array",
- "items": {"type": "string"},
- },
+ "schema": IsDict(
+ {
+ "title": "X-Token",
+ "anyOf": [
+ {"type": "array", "items": {"type": "string"}},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "X-Token",
+ "type": "array",
+ "items": {"type": "string"},
+ }
+ ),
"name": "x-token",
"in": "header",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
-from ...utils import needs_py310
+from ...utils import needs_py39
@pytest.fixture(name="client")
return client
-@needs_py310
+@needs_py39
@pytest.mark.parametrize(
"path,headers,expected_status,expected_response",
[
assert response.json() == expected_response
-@needs_py310
+@needs_py39
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200
- # insert_assert(response.json())
assert response.json() == {
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"parameters": [
{
"required": False,
- "schema": {
- "title": "X-Token",
- "type": "array",
- "items": {"type": "string"},
- },
+ "schema": IsDict(
+ {
+ "title": "X-Token",
+ "anyOf": [
+ {"type": "array", "items": {"type": "string"}},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "X-Token",
+ "type": "array",
+ "items": {"type": "string"},
+ }
+ ),
"name": "x-token",
"in": "header",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200
- # insert_assert(response.json())
assert response.json() == {
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"parameters": [
{
"required": False,
- "schema": {
- "title": "X-Token",
- "type": "array",
- "items": {"type": "string"},
- },
+ "schema": IsDict(
+ {
+ "title": "X-Token",
+ "anyOf": [
+ {"type": "array", "items": {"type": "string"}},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "X-Token",
+ "type": "array",
+ "items": {"type": "string"},
+ }
+ ),
"name": "x-token",
"in": "header",
}
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.openapi_callbacks.tutorial001 import app, invoice_notification
"parameters": [
{
"required": False,
- "schema": {
- "title": "Callback Url",
- "maxLength": 2083,
- "minLength": 1,
- "type": "string",
- "format": "uri",
- },
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "uri",
+ "minLength": 1,
+ "maxLength": 2083,
+ },
+ {"type": "null"},
+ ],
+ "title": "Callback Url",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Callback Url",
+ "maxLength": 2083,
+ "minLength": 1,
+ "type": "string",
+ "format": "uri",
+ }
+ ),
"name": "callback_url",
"in": "query",
}
"type": "object",
"properties": {
"id": {"title": "Id", "type": "string"},
- "title": {"title": "Title", "type": "string"},
+ "title": IsDict(
+ {
+ "title": "Title",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Title", "type": "string"}
+ ),
"customer": {"title": "Customer", "type": "string"},
"total": {"title": "Total", "type": "number"},
},
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.path_operation_advanced_configuration.tutorial004 import app
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
"tags": {
"title": "Tags",
"uniqueItems": True,
+import pytest
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.path_operation_advanced_configuration.tutorial007 import app
+from ...utils import needs_pydanticv2
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.path_operation_advanced_configuration.tutorial007 import app
-def test_post():
+ client = TestClient(app)
+ return client
+
+
+@needs_pydanticv2
+def test_post(client: TestClient):
yaml_data = """
name: Deadpoolio
tags:
}
-def test_post_broken_yaml():
+@needs_pydanticv2
+def test_post_broken_yaml(client: TestClient):
yaml_data = """
name: Deadpoolio
tags:
assert response.json() == {"detail": "Invalid YAML"}
-def test_post_invalid():
+@needs_pydanticv2
+def test_post_invalid(client: TestClient):
yaml_data = """
name: Deadpoolio
tags:
"""
response = client.post("/items/", content=yaml_data)
assert response.status_code == 422, response.text
+ # insert_assert(response.json())
assert response.json() == {
"detail": [
- {"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"}
+ {
+ "type": "string_type",
+ "loc": ["tags", 3],
+ "msg": "Input should be a valid string",
+ "input": {"sneaky": "object"},
+ "url": match_pydantic_error_url("string_type"),
+ }
]
}
-def test_openapi_schema():
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
--- /dev/null
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_pydanticv1
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.path_operation_advanced_configuration.tutorial007_pv1 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_pydanticv1
+def test_post(client: TestClient):
+ yaml_data = """
+ name: Deadpoolio
+ tags:
+ - x-force
+ - x-men
+ - x-avengers
+ """
+ response = client.post("/items/", content=yaml_data)
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "name": "Deadpoolio",
+ "tags": ["x-force", "x-men", "x-avengers"],
+ }
+
+
+@needs_pydanticv1
+def test_post_broken_yaml(client: TestClient):
+ yaml_data = """
+ name: Deadpoolio
+ tags:
+ x - x-force
+ x - x-men
+ x - x-avengers
+ """
+ response = client.post("/items/", content=yaml_data)
+ assert response.status_code == 422, response.text
+ assert response.json() == {"detail": "Invalid YAML"}
+
+
+@needs_pydanticv1
+def test_post_invalid(client: TestClient):
+ yaml_data = """
+ name: Deadpoolio
+ tags:
+ - x-force
+ - x-men
+ - x-avengers
+ - sneaky: object
+ """
+ response = client.post("/items/", content=yaml_data)
+ assert response.status_code == 422, response.text
+ assert response.json() == {
+ "detail": [
+ {"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"}
+ ]
+ }
+
+
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/items/": {
+ "post": {
+ "summary": "Create Item",
+ "operationId": "create_item_items__post",
+ "requestBody": {
+ "content": {
+ "application/x-yaml": {
+ "schema": {
+ "title": "Item",
+ "required": ["name", "tags"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "tags": {
+ "title": "Tags",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ },
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ }
+ },
+ }
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.path_operation_configuration.tutorial005 import app
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
"tags": {
"title": "Tags",
"uniqueItems": True,
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
"tags": {
"title": "Tags",
"uniqueItems": True,
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
"tags": {
"title": "Tags",
"uniqueItems": True,
-import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.path_params.tutorial005 import app
client = TestClient(app)
-@pytest.mark.parametrize(
- "url,status_code,expected",
- [
- (
- "/models/alexnet",
- 200,
- {"model_name": "alexnet", "message": "Deep Learning FTW!"},
- ),
- (
- "/models/lenet",
- 200,
- {"model_name": "lenet", "message": "LeCNN all the images"},
- ),
- (
- "/models/resnet",
- 200,
- {"model_name": "resnet", "message": "Have some residuals"},
- ),
- (
- "/models/foo",
- 422,
- {
- "detail": [
- {
- "ctx": {"enum_values": ["alexnet", "resnet", "lenet"]},
- "loc": ["path", "model_name"],
- "msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'",
- "type": "type_error.enum",
- }
- ]
- },
- ),
- ],
-)
-def test_get_enums(url, status_code, expected):
- response = client.get(url)
- assert response.status_code == status_code
- assert response.json() == expected
+def test_get_enums_alexnet():
+ response = client.get("/models/alexnet")
+ assert response.status_code == 200
+ assert response.json() == {"model_name": "alexnet", "message": "Deep Learning FTW!"}
+
+
+def test_get_enums_lenet():
+ response = client.get("/models/lenet")
+ assert response.status_code == 200
+ assert response.json() == {"model_name": "lenet", "message": "LeCNN all the images"}
-def test_openapi():
+def test_get_enums_resnet():
+ response = client.get("/models/resnet")
+ assert response.status_code == 200
+ assert response.json() == {"model_name": "resnet", "message": "Have some residuals"}
+
+
+def test_get_enums_invalid():
+ response = client.get("/models/foo")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "enum",
+ "loc": ["path", "model_name"],
+ "msg": "Input should be 'alexnet','resnet' or 'lenet'",
+ "input": "foo",
+ "ctx": {"expected": "'alexnet','resnet' or 'lenet'"},
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "ctx": {"enum_values": ["alexnet", "resnet", "lenet"]},
+ "loc": ["path", "model_name"],
+ "msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'",
+ "type": "type_error.enum",
+ }
+ ]
+ }
+ )
+
+
+def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
data = response.json()
}
},
},
- "ModelName": {
- "title": "ModelName",
- "enum": ["alexnet", "resnet", "lenet"],
- "type": "string",
- "description": "An enumeration.",
- },
+ "ModelName": IsDict(
+ {
+ "title": "ModelName",
+ "enum": ["alexnet", "resnet", "lenet"],
+ "type": "string",
+ }
+ )
+ | IsDict(
+ {
+ # TODO: remove when deprecating Pydantic v1
+ "title": "ModelName",
+ "enum": ["alexnet", "resnet", "lenet"],
+ "type": "string",
+ "description": "An enumeration.",
+ }
+ ),
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
-import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from docs_src.query_params.tutorial005 import app
client = TestClient(app)
-query_required = {
- "detail": [
- {
- "loc": ["query", "needy"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
-}
+def test_foo_needy_very():
+ response = client.get("/items/foo?needy=very")
+ assert response.status_code == 200
+ assert response.json() == {"item_id": "foo", "needy": "very"}
-@pytest.mark.parametrize(
- "path,expected_status,expected_response",
- [
- ("/items/foo?needy=very", 200, {"item_id": "foo", "needy": "very"}),
- ("/items/foo", 422, query_required),
- ("/items/foo", 422, query_required),
- ],
-)
-def test(path, expected_status, expected_response):
- response = client.get(path)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_foo_no_needy():
+ response = client.get("/items/foo")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "needy"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "needy"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
def test_openapi_schema():
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.query_params.tutorial006 import app
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.query_params.tutorial006 import app
+ c = TestClient(app)
+ return c
-query_required = {
- "detail": [
- {
- "loc": ["query", "needy"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
-}
+
+def test_foo_needy_very(client: TestClient):
+ response = client.get("/items/foo?needy=very")
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": "foo",
+ "needy": "very",
+ "skip": 0,
+ "limit": None,
+ }
-@pytest.mark.parametrize(
- "path,expected_status,expected_response",
- [
- (
- "/items/foo?needy=very",
- 200,
- {"item_id": "foo", "needy": "very", "skip": 0, "limit": None},
- ),
- (
- "/items/foo?skip=a&limit=b",
- 422,
- {
- "detail": [
- {
- "loc": ["query", "needy"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["query", "skip"],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
- },
- {
- "loc": ["query", "limit"],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
- },
- ]
- },
- ),
- ],
-)
-def test(path, expected_status, expected_response):
- response = client.get(path)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_foo_no_needy(client: TestClient):
+ response = client.get("/items/foo?skip=a&limit=b")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "needy"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "int_parsing",
+ "loc": ["query", "skip"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "a",
+ "url": match_pydantic_error_url("int_parsing"),
+ },
+ {
+ "type": "int_parsing",
+ "loc": ["query", "limit"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "b",
+ "url": match_pydantic_error_url("int_parsing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "needy"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["query", "skip"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ },
+ {
+ "loc": ["query", "limit"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ },
+ ]
+ }
+ )
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == {
},
{
"required": False,
- "schema": {"title": "Limit", "type": "integer"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "integer"}, {"type": "null"}],
+ "title": "Limit",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Limit", "type": "integer"}
+ ),
"name": "limit",
"in": "query",
},
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310
-query_required = {
- "detail": [
- {
- "loc": ["query", "needy"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
-}
-
@pytest.fixture(name="client")
def get_client():
@needs_py310
-@pytest.mark.parametrize(
- "path,expected_status,expected_response",
- [
- (
- "/items/foo?needy=very",
- 200,
- {"item_id": "foo", "needy": "very", "skip": 0, "limit": None},
- ),
- (
- "/items/foo?skip=a&limit=b",
- 422,
- {
- "detail": [
- {
- "loc": ["query", "needy"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["query", "skip"],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
- },
- {
- "loc": ["query", "limit"],
- "msg": "value is not a valid integer",
- "type": "type_error.integer",
- },
- ]
- },
- ),
- ],
-)
-def test(path, expected_status, expected_response, client: TestClient):
- response = client.get(path)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_foo_needy_very(client: TestClient):
+ response = client.get("/items/foo?needy=very")
+ assert response.status_code == 200
+ assert response.json() == {
+ "item_id": "foo",
+ "needy": "very",
+ "skip": 0,
+ "limit": None,
+ }
+
+
+@needs_py310
+def test_foo_no_needy(client: TestClient):
+ response = client.get("/items/foo?skip=a&limit=b")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "needy"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "int_parsing",
+ "loc": ["query", "skip"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "a",
+ "url": match_pydantic_error_url("int_parsing"),
+ },
+ {
+ "type": "int_parsing",
+ "loc": ["query", "limit"],
+ "msg": "Input should be a valid integer, unable to parse string as an integer",
+ "input": "b",
+ "url": match_pydantic_error_url("int_parsing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "needy"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["query", "skip"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ },
+ {
+ "loc": ["query", "limit"],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer",
+ },
+ ]
+ }
+ )
@needs_py310
},
{
"required": False,
- "schema": {"title": "Limit", "type": "integer"},
+ "schema": IsDict(
+ {
+ "anyOf": [{"type": "integer"}, {"type": "null"}],
+ "title": "Limit",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Limit", "type": "integer"}
+ ),
"name": "limit",
"in": "query",
},
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.query_params_str_validations.tutorial010 import app
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.query_params_str_validations.tutorial010 import app
+ client = TestClient(app)
+ return client
-regex_error = {
- "detail": [
- {
- "ctx": {"pattern": "^fixedquery$"},
- "loc": ["query", "item-query"],
- "msg": 'string does not match regex "^fixedquery$"',
- "type": "value_error.str.regex",
- }
- ]
-}
+
+def test_query_params_str_validations_no_query(client: TestClient):
+ response = client.get("/items/")
+ assert response.status_code == 200
+ assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+def test_query_params_str_validations_item_query_fixedquery(client: TestClient):
+ response = client.get("/items/", params={"item-query": "fixedquery"})
+ assert response.status_code == 200
+ assert response.json() == {
+ "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+ "q": "fixedquery",
+ }
+
+
+def test_query_params_str_validations_q_fixedquery(client: TestClient):
+ response = client.get("/items/", params={"q": "fixedquery"})
+ assert response.status_code == 200
+ assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
-@pytest.mark.parametrize(
- "q_name,q,expected_status,expected_response",
- [
- (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
- (
- "item-query",
- "fixedquery",
- 200,
- {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"},
- ),
- ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
- ("item-query", "nonregexquery", 422, regex_error),
- ],
-)
-def test_query_params_str_validations(q_name, q, expected_status, expected_response):
- url = "/items/"
- if q_name and q:
- url = f"{url}?{q_name}={q}"
- response = client.get(url)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_query_params_str_validations_item_query_nonregexquery(client: TestClient):
+ response = client.get("/items/", params={"item-query": "nonregexquery"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "string_pattern_mismatch",
+ "loc": ["query", "item-query"],
+ "msg": "String should match pattern '^fixedquery$'",
+ "input": "nonregexquery",
+ "ctx": {"pattern": "^fixedquery$"},
+ "url": match_pydantic_error_url("string_pattern_mismatch"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "ctx": {"pattern": "^fixedquery$"},
+ "loc": ["query", "item-query"],
+ "msg": 'string does not match regex "^fixedquery$"',
+ "type": "value_error.str.regex",
+ }
+ ]
+ }
+ )
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"description": "Query string for the items to search in the database that have a good match",
"required": False,
"deprecated": True,
- "schema": {
- "title": "Query string",
- "maxLength": 50,
- "minLength": 3,
- "pattern": "^fixedquery$",
- "type": "string",
- "description": "Query string for the items to search in the database that have a good match",
- },
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 50,
+ "pattern": "^fixedquery$",
+ },
+ {"type": "null"},
+ ],
+ "title": "Query string",
+ "description": "Query string for the items to search in the database that have a good match",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Query string",
+ "maxLength": 50,
+ "minLength": 3,
+ "pattern": "^fixedquery$",
+ "type": "string",
+ "description": "Query string for the items to search in the database that have a good match",
+ }
+ ),
"name": "item-query",
"in": "query",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.query_params_str_validations.tutorial010_an import app
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.query_params_str_validations.tutorial010_an import app
+ client = TestClient(app)
+ return client
-regex_error = {
- "detail": [
- {
- "ctx": {"pattern": "^fixedquery$"},
- "loc": ["query", "item-query"],
- "msg": 'string does not match regex "^fixedquery$"',
- "type": "value_error.str.regex",
- }
- ]
-}
+
+def test_query_params_str_validations_no_query(client: TestClient):
+ response = client.get("/items/")
+ assert response.status_code == 200
+ assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+def test_query_params_str_validations_item_query_fixedquery(client: TestClient):
+ response = client.get("/items/", params={"item-query": "fixedquery"})
+ assert response.status_code == 200
+ assert response.json() == {
+ "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+ "q": "fixedquery",
+ }
+
+
+def test_query_params_str_validations_q_fixedquery(client: TestClient):
+ response = client.get("/items/", params={"q": "fixedquery"})
+ assert response.status_code == 200
+ assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
-@pytest.mark.parametrize(
- "q_name,q,expected_status,expected_response",
- [
- (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
- (
- "item-query",
- "fixedquery",
- 200,
- {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"},
- ),
- ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
- ("item-query", "nonregexquery", 422, regex_error),
- ],
-)
-def test_query_params_str_validations(q_name, q, expected_status, expected_response):
- url = "/items/"
- if q_name and q:
- url = f"{url}?{q_name}={q}"
- response = client.get(url)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_query_params_str_validations_item_query_nonregexquery(client: TestClient):
+ response = client.get("/items/", params={"item-query": "nonregexquery"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "string_pattern_mismatch",
+ "loc": ["query", "item-query"],
+ "msg": "String should match pattern '^fixedquery$'",
+ "input": "nonregexquery",
+ "ctx": {"pattern": "^fixedquery$"},
+ "url": match_pydantic_error_url("string_pattern_mismatch"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "ctx": {"pattern": "^fixedquery$"},
+ "loc": ["query", "item-query"],
+ "msg": 'string does not match regex "^fixedquery$"',
+ "type": "value_error.str.regex",
+ }
+ ]
+ }
+ )
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"description": "Query string for the items to search in the database that have a good match",
"required": False,
"deprecated": True,
- "schema": {
- "title": "Query string",
- "maxLength": 50,
- "minLength": 3,
- "pattern": "^fixedquery$",
- "type": "string",
- "description": "Query string for the items to search in the database that have a good match",
- },
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 50,
+ "pattern": "^fixedquery$",
+ },
+ {"type": "null"},
+ ],
+ "title": "Query string",
+ "description": "Query string for the items to search in the database that have a good match",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Query string",
+ "maxLength": 50,
+ "minLength": 3,
+ "pattern": "^fixedquery$",
+ "type": "string",
+ "description": "Query string for the items to search in the database that have a good match",
+ }
+ ),
"name": "item-query",
"in": "query",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310
return client
-regex_error = {
- "detail": [
- {
- "ctx": {"pattern": "^fixedquery$"},
- "loc": ["query", "item-query"],
- "msg": 'string does not match regex "^fixedquery$"',
- "type": "value_error.str.regex",
- }
- ]
-}
+@needs_py310
+def test_query_params_str_validations_no_query(client: TestClient):
+ response = client.get("/items/")
+ assert response.status_code == 200
+ assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+@needs_py310
+def test_query_params_str_validations_item_query_fixedquery(client: TestClient):
+ response = client.get("/items/", params={"item-query": "fixedquery"})
+ assert response.status_code == 200
+ assert response.json() == {
+ "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+ "q": "fixedquery",
+ }
+
+
+@needs_py310
+def test_query_params_str_validations_q_fixedquery(client: TestClient):
+ response = client.get("/items/", params={"q": "fixedquery"})
+ assert response.status_code == 200
+ assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
@needs_py310
-@pytest.mark.parametrize(
- "q_name,q,expected_status,expected_response",
- [
- (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
- (
- "item-query",
- "fixedquery",
- 200,
- {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"},
- ),
- ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
- ("item-query", "nonregexquery", 422, regex_error),
- ],
-)
-def test_query_params_str_validations(
- q_name, q, expected_status, expected_response, client: TestClient
-):
- url = "/items/"
- if q_name and q:
- url = f"{url}?{q_name}={q}"
- response = client.get(url)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_query_params_str_validations_item_query_nonregexquery(client: TestClient):
+ response = client.get("/items/", params={"item-query": "nonregexquery"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "string_pattern_mismatch",
+ "loc": ["query", "item-query"],
+ "msg": "String should match pattern '^fixedquery$'",
+ "input": "nonregexquery",
+ "ctx": {"pattern": "^fixedquery$"},
+ "url": match_pydantic_error_url("string_pattern_mismatch"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "ctx": {"pattern": "^fixedquery$"},
+ "loc": ["query", "item-query"],
+ "msg": 'string does not match regex "^fixedquery$"',
+ "type": "value_error.str.regex",
+ }
+ ]
+ }
+ )
@needs_py310
"description": "Query string for the items to search in the database that have a good match",
"required": False,
"deprecated": True,
- "schema": {
- "title": "Query string",
- "maxLength": 50,
- "minLength": 3,
- "pattern": "^fixedquery$",
- "type": "string",
- "description": "Query string for the items to search in the database that have a good match",
- },
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 50,
+ "pattern": "^fixedquery$",
+ },
+ {"type": "null"},
+ ],
+ "title": "Query string",
+ "description": "Query string for the items to search in the database that have a good match",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Query string",
+ "maxLength": 50,
+ "minLength": 3,
+ "pattern": "^fixedquery$",
+ "type": "string",
+ "description": "Query string for the items to search in the database that have a good match",
+ }
+ ),
"name": "item-query",
"in": "query",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
return client
-regex_error = {
- "detail": [
- {
- "ctx": {"pattern": "^fixedquery$"},
- "loc": ["query", "item-query"],
- "msg": 'string does not match regex "^fixedquery$"',
- "type": "value_error.str.regex",
- }
- ]
-}
+@needs_py39
+def test_query_params_str_validations_no_query(client: TestClient):
+ response = client.get("/items/")
+ assert response.status_code == 200
+ assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+@needs_py39
+def test_query_params_str_validations_item_query_fixedquery(client: TestClient):
+ response = client.get("/items/", params={"item-query": "fixedquery"})
+ assert response.status_code == 200
+ assert response.json() == {
+ "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+ "q": "fixedquery",
+ }
+
+
+@needs_py39
+def test_query_params_str_validations_q_fixedquery(client: TestClient):
+ response = client.get("/items/", params={"q": "fixedquery"})
+ assert response.status_code == 200
+ assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
@needs_py39
-@pytest.mark.parametrize(
- "q_name,q,expected_status,expected_response",
- [
- (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
- (
- "item-query",
- "fixedquery",
- 200,
- {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"},
- ),
- ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
- ("item-query", "nonregexquery", 422, regex_error),
- ],
-)
-def test_query_params_str_validations(
- q_name, q, expected_status, expected_response, client: TestClient
-):
- url = "/items/"
- if q_name and q:
- url = f"{url}?{q_name}={q}"
- response = client.get(url)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_query_params_str_validations_item_query_nonregexquery(client: TestClient):
+ response = client.get("/items/", params={"item-query": "nonregexquery"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "string_pattern_mismatch",
+ "loc": ["query", "item-query"],
+ "msg": "String should match pattern '^fixedquery$'",
+ "input": "nonregexquery",
+ "ctx": {"pattern": "^fixedquery$"},
+ "url": match_pydantic_error_url("string_pattern_mismatch"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "ctx": {"pattern": "^fixedquery$"},
+ "loc": ["query", "item-query"],
+ "msg": 'string does not match regex "^fixedquery$"',
+ "type": "value_error.str.regex",
+ }
+ ]
+ }
+ )
@needs_py39
"description": "Query string for the items to search in the database that have a good match",
"required": False,
"deprecated": True,
- "schema": {
- "title": "Query string",
- "maxLength": 50,
- "minLength": 3,
- "pattern": "^fixedquery$",
- "type": "string",
- "description": "Query string for the items to search in the database that have a good match",
- },
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 50,
+ "pattern": "^fixedquery$",
+ },
+ {"type": "null"},
+ ],
+ "title": "Query string",
+ "description": "Query string for the items to search in the database that have a good match",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Query string",
+ "maxLength": 50,
+ "minLength": 3,
+ "pattern": "^fixedquery$",
+ "type": "string",
+ "description": "Query string for the items to search in the database that have a good match",
+ }
+ ),
"name": "item-query",
"in": "query",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py310
return client
-regex_error = {
- "detail": [
- {
- "ctx": {"pattern": "^fixedquery$"},
- "loc": ["query", "item-query"],
- "msg": 'string does not match regex "^fixedquery$"',
- "type": "value_error.str.regex",
- }
- ]
-}
+@needs_py310
+def test_query_params_str_validations_no_query(client: TestClient):
+ response = client.get("/items/")
+ assert response.status_code == 200
+ assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+@needs_py310
+def test_query_params_str_validations_item_query_fixedquery(client: TestClient):
+ response = client.get("/items/", params={"item-query": "fixedquery"})
+ assert response.status_code == 200
+ assert response.json() == {
+ "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+ "q": "fixedquery",
+ }
+
+
+@needs_py310
+def test_query_params_str_validations_q_fixedquery(client: TestClient):
+ response = client.get("/items/", params={"q": "fixedquery"})
+ assert response.status_code == 200
+ assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
@needs_py310
-@pytest.mark.parametrize(
- "q_name,q,expected_status,expected_response",
- [
- (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
- (
- "item-query",
- "fixedquery",
- 200,
- {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"},
- ),
- ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
- ("item-query", "nonregexquery", 422, regex_error),
- ],
-)
-def test_query_params_str_validations(
- q_name, q, expected_status, expected_response, client: TestClient
-):
- url = "/items/"
- if q_name and q:
- url = f"{url}?{q_name}={q}"
- response = client.get(url)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_query_params_str_validations_item_query_nonregexquery(client: TestClient):
+ response = client.get("/items/", params={"item-query": "nonregexquery"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "string_pattern_mismatch",
+ "loc": ["query", "item-query"],
+ "msg": "String should match pattern '^fixedquery$'",
+ "input": "nonregexquery",
+ "ctx": {"pattern": "^fixedquery$"},
+ "url": match_pydantic_error_url("string_pattern_mismatch"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "ctx": {"pattern": "^fixedquery$"},
+ "loc": ["query", "item-query"],
+ "msg": 'string does not match regex "^fixedquery$"',
+ "type": "value_error.str.regex",
+ }
+ ]
+ }
+ )
@needs_py310
"description": "Query string for the items to search in the database that have a good match",
"required": False,
"deprecated": True,
- "schema": {
- "title": "Query string",
- "maxLength": 50,
- "minLength": 3,
- "pattern": "^fixedquery$",
- "type": "string",
- "description": "Query string for the items to search in the database that have a good match",
- },
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 50,
+ "pattern": "^fixedquery$",
+ },
+ {"type": "null"},
+ ],
+ "title": "Query string",
+ "description": "Query string for the items to search in the database that have a good match",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Query string",
+ "maxLength": 50,
+ "minLength": 3,
+ "pattern": "^fixedquery$",
+ "type": "string",
+ "description": "Query string for the items to search in the database that have a good match",
+ }
+ ),
"name": "item-query",
"in": "query",
}
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.query_params_str_validations.tutorial011 import app
"parameters": [
{
"required": False,
- "schema": {
- "title": "Q",
- "type": "array",
- "items": {"type": "string"},
- },
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {"type": "array", "items": {"type": "string"}},
+ {"type": "null"},
+ ],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Q",
+ "type": "array",
+ "items": {"type": "string"},
+ }
+ ),
"name": "q",
"in": "query",
}
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.query_params_str_validations.tutorial011_an import app
"parameters": [
{
"required": False,
- "schema": {
- "title": "Q",
- "type": "array",
- "items": {"type": "string"},
- },
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {"type": "array", "items": {"type": "string"}},
+ {"type": "null"},
+ ],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Q",
+ "type": "array",
+ "items": {"type": "string"},
+ }
+ ),
"name": "q",
"in": "query",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"parameters": [
{
"required": False,
- "schema": {
- "title": "Q",
- "type": "array",
- "items": {"type": "string"},
- },
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {"type": "array", "items": {"type": "string"}},
+ {"type": "null"},
+ ],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Q",
+ "type": "array",
+ "items": {"type": "string"},
+ }
+ ),
"name": "q",
"in": "query",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
"parameters": [
{
"required": False,
- "schema": {
- "title": "Q",
- "type": "array",
- "items": {"type": "string"},
- },
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {"type": "array", "items": {"type": "string"}},
+ {"type": "null"},
+ ],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Q",
+ "type": "array",
+ "items": {"type": "string"},
+ }
+ ),
"name": "q",
"in": "query",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"parameters": [
{
"required": False,
- "schema": {
- "title": "Q",
- "type": "array",
- "items": {"type": "string"},
- },
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {"type": "array", "items": {"type": "string"}},
+ {"type": "null"},
+ ],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Q",
+ "type": "array",
+ "items": {"type": "string"},
+ }
+ ),
"name": "q",
"in": "query",
}
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
"parameters": [
{
"required": False,
- "schema": {
- "title": "Q",
- "type": "array",
- "items": {"type": "string"},
- },
+ "schema": IsDict(
+ {
+ "anyOf": [
+ {"type": "array", "items": {"type": "string"}},
+ {"type": "null"},
+ ],
+ "title": "Q",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Q",
+ "type": "array",
+ "items": {"type": "string"},
+ }
+ ),
"name": "q",
"in": "query",
}
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from docs_src.request_files.tutorial001 import app
def test_post_form_no_body():
response = client.post("/files/")
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "file"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "file"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
def test_post_body_json():
response = client.post("/files/", json={"file": "Foo"})
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "file"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "file"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
def test_post_file(tmp_path):
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.request_files.tutorial001_02 import app
"requestBody": {
"content": {
"multipart/form-data": {
- "schema": {
- "$ref": "#/components/schemas/Body_create_file_files__post"
- }
+ "schema": IsDict(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Body_create_file_files__post"
+ }
+ ],
+ "title": "Body",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "$ref": "#/components/schemas/Body_create_file_files__post"
+ }
+ )
}
}
},
"requestBody": {
"content": {
"multipart/form-data": {
- "schema": {
- "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
- }
+ "schema": IsDict(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+ }
+ ],
+ "title": "Body",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+ }
+ )
}
}
},
"title": "Body_create_file_files__post",
"type": "object",
"properties": {
- "file": {"title": "File", "type": "string", "format": "binary"}
+ "file": IsDict(
+ {
+ "title": "File",
+ "anyOf": [
+ {"type": "string", "format": "binary"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "File", "type": "string", "format": "binary"}
+ )
},
},
"Body_create_upload_file_uploadfile__post": {
"title": "Body_create_upload_file_uploadfile__post",
"type": "object",
"properties": {
- "file": {"title": "File", "type": "string", "format": "binary"}
+ "file": IsDict(
+ {
+ "title": "File",
+ "anyOf": [
+ {"type": "string", "format": "binary"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "File", "type": "string", "format": "binary"}
+ )
},
},
"HTTPValidationError": {
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.request_files.tutorial001_02_an import app
"requestBody": {
"content": {
"multipart/form-data": {
- "schema": {
- "$ref": "#/components/schemas/Body_create_file_files__post"
- }
+ "schema": IsDict(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Body_create_file_files__post"
+ }
+ ],
+ "title": "Body",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "$ref": "#/components/schemas/Body_create_file_files__post"
+ }
+ )
}
}
},
"requestBody": {
"content": {
"multipart/form-data": {
- "schema": {
- "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
- }
+ "schema": IsDict(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+ }
+ ],
+ "title": "Body",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+ }
+ )
}
}
},
"title": "Body_create_file_files__post",
"type": "object",
"properties": {
- "file": {"title": "File", "type": "string", "format": "binary"}
+ "file": IsDict(
+ {
+ "title": "File",
+ "anyOf": [
+ {"type": "string", "format": "binary"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "File", "type": "string", "format": "binary"}
+ )
},
},
"Body_create_upload_file_uploadfile__post": {
"title": "Body_create_upload_file_uploadfile__post",
"type": "object",
"properties": {
- "file": {"title": "File", "type": "string", "format": "binary"}
+ "file": IsDict(
+ {
+ "title": "File",
+ "anyOf": [
+ {"type": "string", "format": "binary"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "File", "type": "string", "format": "binary"}
+ )
},
},
"HTTPValidationError": {
from pathlib import Path
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"requestBody": {
"content": {
"multipart/form-data": {
- "schema": {
- "$ref": "#/components/schemas/Body_create_file_files__post"
- }
+ "schema": IsDict(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Body_create_file_files__post"
+ }
+ ],
+ "title": "Body",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "$ref": "#/components/schemas/Body_create_file_files__post"
+ }
+ )
}
}
},
"requestBody": {
"content": {
"multipart/form-data": {
- "schema": {
- "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
- }
+ "schema": IsDict(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+ }
+ ],
+ "title": "Body",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+ }
+ )
}
}
},
"title": "Body_create_file_files__post",
"type": "object",
"properties": {
- "file": {"title": "File", "type": "string", "format": "binary"}
+ "file": IsDict(
+ {
+ "title": "File",
+ "anyOf": [
+ {"type": "string", "format": "binary"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "File", "type": "string", "format": "binary"}
+ )
},
},
"Body_create_upload_file_uploadfile__post": {
"title": "Body_create_upload_file_uploadfile__post",
"type": "object",
"properties": {
- "file": {"title": "File", "type": "string", "format": "binary"}
+ "file": IsDict(
+ {
+ "title": "File",
+ "anyOf": [
+ {"type": "string", "format": "binary"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "File", "type": "string", "format": "binary"}
+ )
},
},
"HTTPValidationError": {
from pathlib import Path
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
"requestBody": {
"content": {
"multipart/form-data": {
- "schema": {
- "$ref": "#/components/schemas/Body_create_file_files__post"
- }
+ "schema": IsDict(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Body_create_file_files__post"
+ }
+ ],
+ "title": "Body",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "$ref": "#/components/schemas/Body_create_file_files__post"
+ }
+ )
}
}
},
"requestBody": {
"content": {
"multipart/form-data": {
- "schema": {
- "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
- }
+ "schema": IsDict(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+ }
+ ],
+ "title": "Body",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+ }
+ )
}
}
},
"title": "Body_create_file_files__post",
"type": "object",
"properties": {
- "file": {"title": "File", "type": "string", "format": "binary"}
+ "file": IsDict(
+ {
+ "title": "File",
+ "anyOf": [
+ {"type": "string", "format": "binary"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "File", "type": "string", "format": "binary"}
+ )
},
},
"Body_create_upload_file_uploadfile__post": {
"title": "Body_create_upload_file_uploadfile__post",
"type": "object",
"properties": {
- "file": {"title": "File", "type": "string", "format": "binary"}
+ "file": IsDict(
+ {
+ "title": "File",
+ "anyOf": [
+ {"type": "string", "format": "binary"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "File", "type": "string", "format": "binary"}
+ )
},
},
"HTTPValidationError": {
from pathlib import Path
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"requestBody": {
"content": {
"multipart/form-data": {
- "schema": {
- "$ref": "#/components/schemas/Body_create_file_files__post"
- }
+ "schema": IsDict(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Body_create_file_files__post"
+ }
+ ],
+ "title": "Body",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "$ref": "#/components/schemas/Body_create_file_files__post"
+ }
+ )
}
}
},
"requestBody": {
"content": {
"multipart/form-data": {
- "schema": {
- "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
- }
+ "schema": IsDict(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+ }
+ ],
+ "title": "Body",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+ }
+ )
}
}
},
"title": "Body_create_file_files__post",
"type": "object",
"properties": {
- "file": {"title": "File", "type": "string", "format": "binary"}
+ "file": IsDict(
+ {
+ "title": "File",
+ "anyOf": [
+ {"type": "string", "format": "binary"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "File", "type": "string", "format": "binary"}
+ )
},
},
"Body_create_upload_file_uploadfile__post": {
"title": "Body_create_upload_file_uploadfile__post",
"type": "object",
"properties": {
- "file": {"title": "File", "type": "string", "format": "binary"}
+ "file": IsDict(
+ {
+ "title": "File",
+ "anyOf": [
+ {"type": "string", "format": "binary"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "File", "type": "string", "format": "binary"}
+ )
},
},
"HTTPValidationError": {
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from docs_src.request_files.tutorial001_an import app
client = TestClient(app)
-file_required = {
- "detail": [
- {
- "loc": ["body", "file"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
-}
-
-
def test_post_form_no_body():
response = client.post("/files/")
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "file"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "file"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
def test_post_body_json():
response = client.post("/files/", json={"file": "Foo"})
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "file"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "file"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
def test_post_file(tmp_path):
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
return client
-file_required = {
- "detail": [
- {
- "loc": ["body", "file"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
-}
-
-
@needs_py39
def test_post_form_no_body(client: TestClient):
response = client.post("/files/")
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "file"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "file"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
@needs_py39
def test_post_body_json(client: TestClient):
response = client.post("/files/", json={"file": "Foo"})
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "file"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "file"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
@needs_py39
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from docs_src.request_files.tutorial002 import app
client = TestClient(app)
-file_required = {
- "detail": [
- {
- "loc": ["body", "files"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
-}
-
-
def test_post_form_no_body():
response = client.post("/files/")
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "files"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "files"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
def test_post_body_json():
response = client.post("/files/", json={"file": "Foo"})
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "files"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "files"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
def test_post_files(tmp_path):
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from docs_src.request_files.tutorial002_an import app
client = TestClient(app)
-file_required = {
- "detail": [
- {
- "loc": ["body", "files"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
-}
-
-
def test_post_form_no_body():
response = client.post("/files/")
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "files"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "files"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
def test_post_body_json():
response = client.post("/files/", json={"file": "Foo"})
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "files"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "files"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
def test_post_files(tmp_path):
import pytest
+from dirty_equals import IsDict
from fastapi import FastAPI
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
return client
-file_required = {
- "detail": [
- {
- "loc": ["body", "files"],
- "msg": "field required",
- "type": "value_error.missing",
- }
- ]
-}
-
-
@needs_py39
def test_post_form_no_body(client: TestClient):
response = client.post("/files/")
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "files"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "files"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
@needs_py39
def test_post_body_json(client: TestClient):
response = client.post("/files/", json={"file": "Foo"})
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "files"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "files"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
@needs_py39
import pytest
+from dirty_equals import IsDict
from fastapi import FastAPI
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
def test_post_form_no_body(client: TestClient):
response = client.post("/files/")
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "files"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "files"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
@needs_py39
def test_post_body_json(client: TestClient):
response = client.post("/files/", json={"file": "Foo"})
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "files"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "files"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
@needs_py39
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.request_forms.tutorial001 import app
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.request_forms.tutorial001 import app
+ client = TestClient(app)
+ return client
-password_required = {
- "detail": [
+
+def test_post_body_form(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo", "password": "secret"})
+ assert response.status_code == 200
+ assert response.json() == {"username": "Foo"}
+
+
+def test_post_body_form_no_password(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "password"],
- "msg": "field required",
- "type": "value_error.missing",
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
}
- ]
-}
-username_required = {
- "detail": [
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["body", "username"],
- "msg": "field required",
- "type": "value_error.missing",
+ "detail": [
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
}
- ]
-}
-username_and_password_required = {
- "detail": [
+ )
+
+
+def test_post_body_form_no_username(client: TestClient):
+ response = client.post("/login/", data={"password": "secret"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "username"],
- "msg": "field required",
- "type": "value_error.missing",
- },
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["body", "password"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/login/",
- {"username": "Foo", "password": "secret"},
- 200,
- {"username": "Foo"},
- ),
- ("/login/", {"username": "Foo"}, 422, password_required),
- ("/login/", {"password": "secret"}, 422, username_required),
- ("/login/", None, 422, username_and_password_required),
- ],
-)
-def test_post_body_form(path, body, expected_status, expected_response):
- response = client.post(path, data=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_post_body_form_no_data(client: TestClient):
+ response = client.post("/login/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-def test_post_body_json():
+def test_post_body_json(client: TestClient):
response = client.post("/login/", json={"username": "Foo", "password": "secret"})
assert response.status_code == 422, response.text
- assert response.json() == username_and_password_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.request_forms.tutorial001_an import app
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.request_forms.tutorial001_an import app
+ client = TestClient(app)
+ return client
-password_required = {
- "detail": [
+
+def test_post_body_form(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo", "password": "secret"})
+ assert response.status_code == 200
+ assert response.json() == {"username": "Foo"}
+
+
+def test_post_body_form_no_password(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "password"],
- "msg": "field required",
- "type": "value_error.missing",
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
}
- ]
-}
-username_required = {
- "detail": [
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["body", "username"],
- "msg": "field required",
- "type": "value_error.missing",
+ "detail": [
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
}
- ]
-}
-username_and_password_required = {
- "detail": [
+ )
+
+
+def test_post_body_form_no_username(client: TestClient):
+ response = client.post("/login/", data={"password": "secret"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "username"],
- "msg": "field required",
- "type": "value_error.missing",
- },
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["body", "password"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/login/",
- {"username": "Foo", "password": "secret"},
- 200,
- {"username": "Foo"},
- ),
- ("/login/", {"username": "Foo"}, 422, password_required),
- ("/login/", {"password": "secret"}, 422, username_required),
- ("/login/", None, 422, username_and_password_required),
- ],
-)
-def test_post_body_form(path, body, expected_status, expected_response):
- response = client.post(path, data=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_post_body_form_no_data(client: TestClient):
+ response = client.post("/login/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-def test_post_body_json():
+def test_post_body_json(client: TestClient):
response = client.post("/login/", json={"username": "Foo", "password": "secret"})
assert response.status_code == 422, response.text
- assert response.json() == username_and_password_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
return client
-password_required = {
- "detail": [
+@needs_py39
+def test_post_body_form(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo", "password": "secret"})
+ assert response.status_code == 200
+ assert response.json() == {"username": "Foo"}
+
+
+@needs_py39
+def test_post_body_form_no_password(client: TestClient):
+ response = client.post("/login/", data={"username": "Foo"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "password"],
- "msg": "field required",
- "type": "value_error.missing",
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
}
- ]
-}
-username_required = {
- "detail": [
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["body", "username"],
- "msg": "field required",
- "type": "value_error.missing",
+ "detail": [
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
}
- ]
-}
-username_and_password_required = {
- "detail": [
+ )
+
+
+@needs_py39
+def test_post_body_form_no_username(client: TestClient):
+ response = client.post("/login/", data={"password": "secret"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
{
- "loc": ["body", "username"],
- "msg": "field required",
- "type": "value_error.missing",
- },
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
{
- "loc": ["body", "password"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
@needs_py39
-@pytest.mark.parametrize(
- "path,body,expected_status,expected_response",
- [
- (
- "/login/",
- {"username": "Foo", "password": "secret"},
- 200,
- {"username": "Foo"},
- ),
- ("/login/", {"username": "Foo"}, 422, password_required),
- ("/login/", {"password": "secret"}, 422, username_required),
- ("/login/", None, 422, username_and_password_required),
- ],
-)
-def test_post_body_form(
- path, body, expected_status, expected_response, client: TestClient
-):
- response = client.post(path, data=body)
- assert response.status_code == expected_status
- assert response.json() == expected_response
+def test_post_body_form_no_data(client: TestClient):
+ response = client.post("/login/")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
@needs_py39
def test_post_body_json(client: TestClient):
response = client.post("/login/", json={"username": "Foo", "password": "secret"})
assert response.status_code == 422, response.text
- assert response.json() == username_and_password_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "username"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "password"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "username"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "password"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
@needs_py39
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.request_forms_and_files.tutorial001 import app
-client = TestClient(app)
+@pytest.fixture(name="app")
+def get_app():
+ from docs_src.request_forms_and_files.tutorial001 import app
+ return app
-file_required = {
- "detail": [
- {
- "loc": ["body", "file"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "fileb"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
-
-token_required = {
- "detail": [
- {
- "loc": ["body", "fileb"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
-# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]}
-
-file_and_token_required = {
- "detail": [
- {
- "loc": ["body", "file"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "fileb"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
+@pytest.fixture(name="client")
+def get_client(app: FastAPI):
+ client = TestClient(app)
+ return client
-def test_post_form_no_body():
+def test_post_form_no_body(client: TestClient):
response = client.post("/files/")
assert response.status_code == 422, response.text
- assert response.json() == file_and_token_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "file"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "fileb"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "file"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "fileb"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-def test_post_form_no_file():
+def test_post_form_no_file(client: TestClient):
response = client.post("/files/", data={"token": "foo"})
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "file"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "fileb"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "file"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "fileb"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-def test_post_body_json():
+def test_post_body_json(client: TestClient):
response = client.post("/files/", json={"file": "Foo", "token": "Bar"})
assert response.status_code == 422, response.text
- assert response.json() == file_and_token_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "file"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "fileb"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "file"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "fileb"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-def test_post_file_no_token(tmp_path):
+def test_post_file_no_token(tmp_path, app: FastAPI):
path = tmp_path / "test.txt"
path.write_bytes(b"<file content>")
with path.open("rb") as file:
response = client.post("/files/", files={"file": file})
assert response.status_code == 422, response.text
- assert response.json() == token_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "fileb"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "fileb"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-def test_post_files_and_token(tmp_path):
+def test_post_files_and_token(tmp_path, app: FastAPI):
patha = tmp_path / "test.txt"
pathb = tmp_path / "testb.txt"
patha.write_text("<file content>")
}
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
-from docs_src.request_forms_and_files.tutorial001_an import app
-client = TestClient(app)
+@pytest.fixture(name="app")
+def get_app():
+ from docs_src.request_forms_and_files.tutorial001_an import app
+ return app
-file_required = {
- "detail": [
- {
- "loc": ["body", "file"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "fileb"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
-
-token_required = {
- "detail": [
- {
- "loc": ["body", "fileb"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
-# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]}
-
-file_and_token_required = {
- "detail": [
- {
- "loc": ["body", "file"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "fileb"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
+@pytest.fixture(name="client")
+def get_client(app: FastAPI):
+ client = TestClient(app)
+ return client
-def test_post_form_no_body():
+def test_post_form_no_body(client: TestClient):
response = client.post("/files/")
assert response.status_code == 422, response.text
- assert response.json() == file_and_token_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "file"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "fileb"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "file"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "fileb"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-def test_post_form_no_file():
+def test_post_form_no_file(client: TestClient):
response = client.post("/files/", data={"token": "foo"})
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "file"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "fileb"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "file"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "fileb"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-def test_post_body_json():
+def test_post_body_json(client: TestClient):
response = client.post("/files/", json={"file": "Foo", "token": "Bar"})
assert response.status_code == 422, response.text
- assert response.json() == file_and_token_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "file"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "fileb"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "file"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "fileb"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-def test_post_file_no_token(tmp_path):
+def test_post_file_no_token(tmp_path, app: FastAPI):
path = tmp_path / "test.txt"
path.write_bytes(b"<file content>")
with path.open("rb") as file:
response = client.post("/files/", files={"file": file})
assert response.status_code == 422, response.text
- assert response.json() == token_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "fileb"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "fileb"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
-def test_post_files_and_token(tmp_path):
+def test_post_files_and_token(tmp_path, app: FastAPI):
patha = tmp_path / "test.txt"
pathb = tmp_path / "testb.txt"
patha.write_text("<file content>")
}
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
import pytest
+from dirty_equals import IsDict
from fastapi import FastAPI
from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
from ...utils import needs_py39
return client
-file_required = {
- "detail": [
- {
- "loc": ["body", "file"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "fileb"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
-
-token_required = {
- "detail": [
- {
- "loc": ["body", "fileb"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
-
-# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]}
-
-file_and_token_required = {
- "detail": [
- {
- "loc": ["body", "file"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "fileb"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- {
- "loc": ["body", "token"],
- "msg": "field required",
- "type": "value_error.missing",
- },
- ]
-}
-
-
@needs_py39
def test_post_form_no_body(client: TestClient):
response = client.post("/files/")
assert response.status_code == 422, response.text
- assert response.json() == file_and_token_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "file"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "fileb"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "file"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "fileb"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
@needs_py39
def test_post_form_no_file(client: TestClient):
response = client.post("/files/", data={"token": "foo"})
assert response.status_code == 422, response.text
- assert response.json() == file_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "file"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "fileb"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "file"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "fileb"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
@needs_py39
def test_post_body_json(client: TestClient):
response = client.post("/files/", json={"file": "Foo", "token": "Bar"})
assert response.status_code == 422, response.text
- assert response.json() == file_and_token_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "file"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "fileb"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "file"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "fileb"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
@needs_py39
with path.open("rb") as file:
response = client.post("/files/", files={"file": file})
assert response.status_code == 422, response.text
- assert response.json() == token_required
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "fileb"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ {
+ "type": "missing",
+ "loc": ["body", "token"],
+ "msg": "Field required",
+ "input": None,
+ "url": match_pydantic_error_url("missing"),
+ },
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "fileb"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ {
+ "loc": ["body", "token"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ]
+ }
+ )
@needs_py39
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.response_model.tutorial003 import app
"type": "string",
"format": "email",
},
- "full_name": {"title": "Full Name", "type": "string"},
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
},
},
"UserIn": {
"type": "string",
"format": "email",
},
- "full_name": {"title": "Full Name", "type": "string"},
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
},
},
"ValidationError": {
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.response_model.tutorial003_01 import app
"type": "string",
"format": "email",
},
- "full_name": {"title": "Full Name", "type": "string"},
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
},
},
"HTTPValidationError": {
"type": "string",
"format": "email",
},
- "full_name": {"title": "Full Name", "type": "string"},
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
"password": {"title": "Password", "type": "string"},
},
},
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"type": "string",
"format": "email",
},
- "full_name": {"title": "Full Name", "type": "string"},
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
},
},
"HTTPValidationError": {
"type": "string",
"format": "email",
},
- "full_name": {"title": "Full Name", "type": "string"},
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
"password": {"title": "Password", "type": "string"},
},
},
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"type": "string",
"format": "email",
},
- "full_name": {"title": "Full Name", "type": "string"},
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
},
},
"UserIn": {
"type": "string",
"format": "email",
},
- "full_name": {"title": "Full Name", "type": "string"},
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.response_model.tutorial004 import app
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": {
"title": "Tags",
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": {
"title": "Tags",
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": {
"title": "Tags",
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.response_model.tutorial005 import app
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"tax": {"title": "Tax", "type": "number", "default": 10.5},
},
},
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"tax": {"title": "Tax", "type": "number", "default": 10.5},
},
},
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.response_model.tutorial006 import app
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"tax": {"title": "Tax", "type": "number", "default": 10.5},
},
},
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"tax": {"title": "Tax", "type": "number", "default": 10.5},
},
},
--- /dev/null
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.schema_extra_example.tutorial001 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_pydanticv2
+def test_post_body_example(client: TestClient):
+ response = client.put(
+ "/items/5",
+ json={
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ },
+ )
+ assert response.status_code == 200
+
+
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ # insert_assert(response.json())
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/items/{item_id}": {
+ "put": {
+ "summary": "Update Item",
+ "operationId": "update_item_items__item_id__put",
+ "parameters": [
+ {
+ "name": "item_id",
+ "in": "path",
+ "required": True,
+ "schema": {"type": "integer", "title": "Item Id"},
+ }
+ ],
+ "requestBody": {
+ "required": True,
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/Item"}
+ }
+ },
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "Item": {
+ "properties": {
+ "name": {"type": "string", "title": "Name"},
+ "description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Description",
+ },
+ "price": {"type": "number", "title": "Price"},
+ "tax": {
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ "title": "Tax",
+ },
+ },
+ "type": "object",
+ "required": ["name", "price"],
+ "title": "Item",
+ "examples": [
+ {
+ "description": "A very nice Item",
+ "name": "Foo",
+ "price": 35.4,
+ "tax": 3.2,
+ }
+ ],
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }
--- /dev/null
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_pydanticv1
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.schema_extra_example.tutorial001_pv1 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_pydanticv1
+def test_post_body_example(client: TestClient):
+ response = client.put(
+ "/items/5",
+ json={
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ },
+ )
+ assert response.status_code == 200
+
+
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ # insert_assert(response.json())
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/items/{item_id}": {
+ "put": {
+ "summary": "Update Item",
+ "operationId": "update_item_items__item_id__put",
+ "parameters": [
+ {
+ "required": True,
+ "schema": {"type": "integer", "title": "Item Id"},
+ "name": "item_id",
+ "in": "path",
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/Item"}
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "Item": {
+ "properties": {
+ "name": {"type": "string", "title": "Name"},
+ "description": {"type": "string", "title": "Description"},
+ "price": {"type": "number", "title": "Price"},
+ "tax": {"type": "number", "title": "Tax"},
+ },
+ "type": "object",
+ "required": ["name", "price"],
+ "title": "Item",
+ "examples": [
+ {
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ }
+ ],
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }
--- /dev/null
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310, needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.schema_extra_example.tutorial001_py310 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_py310
+@needs_pydanticv2
+def test_post_body_example(client: TestClient):
+ response = client.put(
+ "/items/5",
+ json={
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ },
+ )
+ assert response.status_code == 200
+
+
+@needs_py310
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ # insert_assert(response.json())
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/items/{item_id}": {
+ "put": {
+ "summary": "Update Item",
+ "operationId": "update_item_items__item_id__put",
+ "parameters": [
+ {
+ "name": "item_id",
+ "in": "path",
+ "required": True,
+ "schema": {"type": "integer", "title": "Item Id"},
+ }
+ ],
+ "requestBody": {
+ "required": True,
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/Item"}
+ }
+ },
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "Item": {
+ "properties": {
+ "name": {"type": "string", "title": "Name"},
+ "description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Description",
+ },
+ "price": {"type": "number", "title": "Price"},
+ "tax": {
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ "title": "Tax",
+ },
+ },
+ "type": "object",
+ "required": ["name", "price"],
+ "title": "Item",
+ "examples": [
+ {
+ "description": "A very nice Item",
+ "name": "Foo",
+ "price": 35.4,
+ "tax": 3.2,
+ }
+ ],
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }
--- /dev/null
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310, needs_pydanticv1
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.schema_extra_example.tutorial001_py310_pv1 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_py310
+@needs_pydanticv1
+def test_post_body_example(client: TestClient):
+ response = client.put(
+ "/items/5",
+ json={
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ },
+ )
+ assert response.status_code == 200
+
+
+@needs_py310
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ # insert_assert(response.json())
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/items/{item_id}": {
+ "put": {
+ "summary": "Update Item",
+ "operationId": "update_item_items__item_id__put",
+ "parameters": [
+ {
+ "required": True,
+ "schema": {"type": "integer", "title": "Item Id"},
+ "name": "item_id",
+ "in": "path",
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/Item"}
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "Item": {
+ "properties": {
+ "name": {"type": "string", "title": "Name"},
+ "description": {"type": "string", "title": "Description"},
+ "price": {"type": "number", "title": "Price"},
+ "tax": {"type": "number", "title": "Tax"},
+ },
+ "type": "object",
+ "required": ["name", "price"],
+ "title": "Item",
+ "examples": [
+ {
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ }
+ ],
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.schema_extra_example.tutorial004 import app
"requestBody": {
"content": {
"application/json": {
- "schema": {
- "allOf": [{"$ref": "#/components/schemas/Item"}],
- "title": "Item",
- "examples": [
- {
- "name": "Foo",
- "description": "A very nice Item",
- "price": 35.4,
- "tax": 3.2,
- },
- {"name": "Bar", "price": "35.4"},
- {
- "name": "Baz",
- "price": "thirty five point four",
- },
- ],
- }
+ "schema": IsDict(
+ {
+ "$ref": "#/components/schemas/Item",
+ "examples": [
+ {
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ },
+ {"name": "Bar", "price": "35.4"},
+ {
+ "name": "Baz",
+ "price": "thirty five point four",
+ },
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "allOf": [
+ {"$ref": "#/components/schemas/Item"}
+ ],
+ "title": "Item",
+ "examples": [
+ {
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ },
+ {"name": "Bar", "price": "35.4"},
+ {
+ "name": "Baz",
+ "price": "thirty five point four",
+ },
+ ],
+ }
+ )
}
},
"required": True,
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"ValidationError": {
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.schema_extra_example.tutorial004_an import app
"requestBody": {
"content": {
"application/json": {
- "schema": {
- "allOf": [{"$ref": "#/components/schemas/Item"}],
- "title": "Item",
- "examples": [
- {
- "name": "Foo",
- "description": "A very nice Item",
- "price": 35.4,
- "tax": 3.2,
- },
- {"name": "Bar", "price": "35.4"},
- {
- "name": "Baz",
- "price": "thirty five point four",
- },
- ],
- }
+ "schema": IsDict(
+ {
+ "$ref": "#/components/schemas/Item",
+ "examples": [
+ {
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ },
+ {"name": "Bar", "price": "35.4"},
+ {
+ "name": "Baz",
+ "price": "thirty five point four",
+ },
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "allOf": [
+ {"$ref": "#/components/schemas/Item"}
+ ],
+ "title": "Item",
+ "examples": [
+ {
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ },
+ {"name": "Bar", "price": "35.4"},
+ {
+ "name": "Baz",
+ "price": "thirty five point four",
+ },
+ ],
+ }
+ )
}
},
"required": True,
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"requestBody": {
"content": {
"application/json": {
- "schema": {
- "allOf": [{"$ref": "#/components/schemas/Item"}],
- "title": "Item",
- "examples": [
- {
- "name": "Foo",
- "description": "A very nice Item",
- "price": 35.4,
- "tax": 3.2,
- },
- {"name": "Bar", "price": "35.4"},
- {
- "name": "Baz",
- "price": "thirty five point four",
- },
- ],
- }
+ "schema": IsDict(
+ {
+ "$ref": "#/components/schemas/Item",
+ "examples": [
+ {
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ },
+ {"name": "Bar", "price": "35.4"},
+ {
+ "name": "Baz",
+ "price": "thirty five point four",
+ },
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "allOf": [
+ {"$ref": "#/components/schemas/Item"}
+ ],
+ "title": "Item",
+ "examples": [
+ {
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ },
+ {"name": "Bar", "price": "35.4"},
+ {
+ "name": "Baz",
+ "price": "thirty five point four",
+ },
+ ],
+ }
+ )
}
},
"required": True,
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
"requestBody": {
"content": {
"application/json": {
- "schema": {
- "allOf": [{"$ref": "#/components/schemas/Item"}],
- "title": "Item",
- "examples": [
- {
- "name": "Foo",
- "description": "A very nice Item",
- "price": 35.4,
- "tax": 3.2,
- },
- {"name": "Bar", "price": "35.4"},
- {
- "name": "Baz",
- "price": "thirty five point four",
- },
- ],
- }
+ "schema": IsDict(
+ {
+ "$ref": "#/components/schemas/Item",
+ "examples": [
+ {
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ },
+ {"name": "Bar", "price": "35.4"},
+ {
+ "name": "Baz",
+ "price": "thirty five point four",
+ },
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "allOf": [
+ {"$ref": "#/components/schemas/Item"}
+ ],
+ "title": "Item",
+ "examples": [
+ {
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ },
+ {"name": "Bar", "price": "35.4"},
+ {
+ "name": "Baz",
+ "price": "thirty five point four",
+ },
+ ],
+ }
+ )
}
},
"required": True,
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"requestBody": {
"content": {
"application/json": {
- "schema": {
- "allOf": [{"$ref": "#/components/schemas/Item"}],
- "title": "Item",
- "examples": [
- {
- "name": "Foo",
- "description": "A very nice Item",
- "price": 35.4,
- "tax": 3.2,
- },
- {"name": "Bar", "price": "35.4"},
- {
- "name": "Baz",
- "price": "thirty five point four",
- },
- ],
- }
+ "schema": IsDict(
+ {
+ "$ref": "#/components/schemas/Item",
+ "examples": [
+ {
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ },
+ {"name": "Bar", "price": "35.4"},
+ {
+ "name": "Baz",
+ "price": "thirty five point four",
+ },
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "allOf": [
+ {"$ref": "#/components/schemas/Item"}
+ ],
+ "title": "Item",
+ "examples": [
+ {
+ "name": "Foo",
+ "description": "A very nice Item",
+ "price": 35.4,
+ "tax": 3.2,
+ },
+ {"name": "Bar", "price": "35.4"},
+ {
+ "name": "Baz",
+ "price": "thirty five point four",
+ },
+ ],
+ }
+ )
}
},
"required": True,
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
"price": {"title": "Price", "type": "number"},
- "tax": {"title": "Tax", "type": "number"},
+ "tax": IsDict(
+ {
+ "title": "Tax",
+ "anyOf": [{"type": "number"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Tax", "type": "number"}
+ ),
},
},
"ValidationError": {
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.security.tutorial003 import app
"required": ["username", "password"],
"type": "object",
"properties": {
- "grant_type": {
- "title": "Grant Type",
- "pattern": "password",
- "type": "string",
- },
+ "grant_type": IsDict(
+ {
+ "title": "Grant Type",
+ "anyOf": [
+ {"pattern": "password", "type": "string"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Grant Type",
+ "pattern": "password",
+ "type": "string",
+ }
+ ),
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
- "client_id": {"title": "Client Id", "type": "string"},
- "client_secret": {"title": "Client Secret", "type": "string"},
+ "client_id": IsDict(
+ {
+ "title": "Client Id",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Id", "type": "string"}
+ ),
+ "client_secret": IsDict(
+ {
+ "title": "Client Secret",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Secret", "type": "string"}
+ ),
},
},
"ValidationError": {
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.security.tutorial003_an import app
"required": ["username", "password"],
"type": "object",
"properties": {
- "grant_type": {
- "title": "Grant Type",
- "pattern": "password",
- "type": "string",
- },
+ "grant_type": IsDict(
+ {
+ "title": "Grant Type",
+ "anyOf": [
+ {"pattern": "password", "type": "string"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Grant Type",
+ "pattern": "password",
+ "type": "string",
+ }
+ ),
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
- "client_id": {"title": "Client Id", "type": "string"},
- "client_secret": {"title": "Client Secret", "type": "string"},
+ "client_id": IsDict(
+ {
+ "title": "Client Id",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Id", "type": "string"}
+ ),
+ "client_secret": IsDict(
+ {
+ "title": "Client Secret",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Secret", "type": "string"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"required": ["username", "password"],
"type": "object",
"properties": {
- "grant_type": {
- "title": "Grant Type",
- "pattern": "password",
- "type": "string",
- },
+ "grant_type": IsDict(
+ {
+ "title": "Grant Type",
+ "anyOf": [
+ {"pattern": "password", "type": "string"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Grant Type",
+ "pattern": "password",
+ "type": "string",
+ }
+ ),
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
- "client_id": {"title": "Client Id", "type": "string"},
- "client_secret": {"title": "Client Secret", "type": "string"},
+ "client_id": IsDict(
+ {
+ "title": "Client Id",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Id", "type": "string"}
+ ),
+ "client_secret": IsDict(
+ {
+ "title": "Client Secret",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Secret", "type": "string"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
"required": ["username", "password"],
"type": "object",
"properties": {
- "grant_type": {
- "title": "Grant Type",
- "pattern": "password",
- "type": "string",
- },
+ "grant_type": IsDict(
+ {
+ "title": "Grant Type",
+ "anyOf": [
+ {"pattern": "password", "type": "string"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Grant Type",
+ "pattern": "password",
+ "type": "string",
+ }
+ ),
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
- "client_id": {"title": "Client Id", "type": "string"},
- "client_secret": {"title": "Client Secret", "type": "string"},
+ "client_id": IsDict(
+ {
+ "title": "Client Id",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Id", "type": "string"}
+ ),
+ "client_secret": IsDict(
+ {
+ "title": "Client Secret",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Secret", "type": "string"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"required": ["username", "password"],
"type": "object",
"properties": {
- "grant_type": {
- "title": "Grant Type",
- "pattern": "password",
- "type": "string",
- },
+ "grant_type": IsDict(
+ {
+ "title": "Grant Type",
+ "anyOf": [
+ {"pattern": "password", "type": "string"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Grant Type",
+ "pattern": "password",
+ "type": "string",
+ }
+ ),
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
- "client_id": {"title": "Client Id", "type": "string"},
- "client_secret": {"title": "Client Secret", "type": "string"},
+ "client_id": IsDict(
+ {
+ "title": "Client Id",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Id", "type": "string"}
+ ),
+ "client_secret": IsDict(
+ {
+ "title": "Client Secret",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Secret", "type": "string"}
+ ),
},
},
"ValidationError": {
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.security.tutorial005 import (
"type": "object",
"properties": {
"username": {"title": "Username", "type": "string"},
- "email": {"title": "Email", "type": "string"},
- "full_name": {"title": "Full Name", "type": "string"},
- "disabled": {"title": "Disabled", "type": "boolean"},
+ "email": IsDict(
+ {
+ "title": "Email",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Email", "type": "string"}
+ ),
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
+ "disabled": IsDict(
+ {
+ "title": "Disabled",
+ "anyOf": [{"type": "boolean"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Disabled", "type": "boolean"}
+ ),
},
},
"Token": {
"required": ["username", "password"],
"type": "object",
"properties": {
- "grant_type": {
- "title": "Grant Type",
- "pattern": "password",
- "type": "string",
- },
+ "grant_type": IsDict(
+ {
+ "title": "Grant Type",
+ "anyOf": [
+ {"pattern": "password", "type": "string"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Grant Type",
+ "pattern": "password",
+ "type": "string",
+ }
+ ),
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
- "client_id": {"title": "Client Id", "type": "string"},
- "client_secret": {"title": "Client Secret", "type": "string"},
+ "client_id": IsDict(
+ {
+ "title": "Client Id",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Id", "type": "string"}
+ ),
+ "client_secret": IsDict(
+ {
+ "title": "Client Secret",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Secret", "type": "string"}
+ ),
},
},
"ValidationError": {
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from docs_src.security.tutorial005_an import (
"type": "object",
"properties": {
"username": {"title": "Username", "type": "string"},
- "email": {"title": "Email", "type": "string"},
- "full_name": {"title": "Full Name", "type": "string"},
- "disabled": {"title": "Disabled", "type": "boolean"},
+ "email": IsDict(
+ {
+ "title": "Email",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Email", "type": "string"}
+ ),
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
+ "disabled": IsDict(
+ {
+ "title": "Disabled",
+ "anyOf": [{"type": "boolean"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Disabled", "type": "boolean"}
+ ),
},
},
"Token": {
"required": ["username", "password"],
"type": "object",
"properties": {
- "grant_type": {
- "title": "Grant Type",
- "pattern": "password",
- "type": "string",
- },
+ "grant_type": IsDict(
+ {
+ "title": "Grant Type",
+ "anyOf": [
+ {"pattern": "password", "type": "string"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Grant Type",
+ "pattern": "password",
+ "type": "string",
+ }
+ ),
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
- "client_id": {"title": "Client Id", "type": "string"},
- "client_secret": {"title": "Client Secret", "type": "string"},
+ "client_id": IsDict(
+ {
+ "title": "Client Id",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Id", "type": "string"}
+ ),
+ "client_secret": IsDict(
+ {
+ "title": "Client Secret",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Secret", "type": "string"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"type": "object",
"properties": {
"username": {"title": "Username", "type": "string"},
- "email": {"title": "Email", "type": "string"},
- "full_name": {"title": "Full Name", "type": "string"},
- "disabled": {"title": "Disabled", "type": "boolean"},
+ "email": IsDict(
+ {
+ "title": "Email",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Email", "type": "string"}
+ ),
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
+ "disabled": IsDict(
+ {
+ "title": "Disabled",
+ "anyOf": [{"type": "boolean"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Disabled", "type": "boolean"}
+ ),
},
},
"Token": {
"required": ["username", "password"],
"type": "object",
"properties": {
- "grant_type": {
- "title": "Grant Type",
- "pattern": "password",
- "type": "string",
- },
+ "grant_type": IsDict(
+ {
+ "title": "Grant Type",
+ "anyOf": [
+ {"pattern": "password", "type": "string"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Grant Type",
+ "pattern": "password",
+ "type": "string",
+ }
+ ),
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
- "client_id": {"title": "Client Id", "type": "string"},
- "client_secret": {"title": "Client Secret", "type": "string"},
+ "client_id": IsDict(
+ {
+ "title": "Client Id",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Id", "type": "string"}
+ ),
+ "client_secret": IsDict(
+ {
+ "title": "Client Secret",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Secret", "type": "string"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
"type": "object",
"properties": {
"username": {"title": "Username", "type": "string"},
- "email": {"title": "Email", "type": "string"},
- "full_name": {"title": "Full Name", "type": "string"},
- "disabled": {"title": "Disabled", "type": "boolean"},
+ "email": IsDict(
+ {
+ "title": "Email",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Email", "type": "string"}
+ ),
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
+ "disabled": IsDict(
+ {
+ "title": "Disabled",
+ "anyOf": [{"type": "boolean"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Disabled", "type": "boolean"}
+ ),
},
},
"Token": {
"required": ["username", "password"],
"type": "object",
"properties": {
- "grant_type": {
- "title": "Grant Type",
- "pattern": "password",
- "type": "string",
- },
+ "grant_type": IsDict(
+ {
+ "title": "Grant Type",
+ "anyOf": [
+ {"pattern": "password", "type": "string"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Grant Type",
+ "pattern": "password",
+ "type": "string",
+ }
+ ),
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
- "client_id": {"title": "Client Id", "type": "string"},
- "client_secret": {"title": "Client Secret", "type": "string"},
+ "client_id": IsDict(
+ {
+ "title": "Client Id",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Id", "type": "string"}
+ ),
+ "client_secret": IsDict(
+ {
+ "title": "Client Secret",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Secret", "type": "string"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py310
"type": "object",
"properties": {
"username": {"title": "Username", "type": "string"},
- "email": {"title": "Email", "type": "string"},
- "full_name": {"title": "Full Name", "type": "string"},
- "disabled": {"title": "Disabled", "type": "boolean"},
+ "email": IsDict(
+ {
+ "title": "Email",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Email", "type": "string"}
+ ),
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
+ "disabled": IsDict(
+ {
+ "title": "Disabled",
+ "anyOf": [{"type": "boolean"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Disabled", "type": "boolean"}
+ ),
},
},
"Token": {
"required": ["username", "password"],
"type": "object",
"properties": {
- "grant_type": {
- "title": "Grant Type",
- "pattern": "password",
- "type": "string",
- },
+ "grant_type": IsDict(
+ {
+ "title": "Grant Type",
+ "anyOf": [
+ {"pattern": "password", "type": "string"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Grant Type",
+ "pattern": "password",
+ "type": "string",
+ }
+ ),
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
- "client_id": {"title": "Client Id", "type": "string"},
- "client_secret": {"title": "Client Secret", "type": "string"},
+ "client_id": IsDict(
+ {
+ "title": "Client Id",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Id", "type": "string"}
+ ),
+ "client_secret": IsDict(
+ {
+ "title": "Client Secret",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Secret", "type": "string"}
+ ),
},
},
"ValidationError": {
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
from ...utils import needs_py39
"type": "object",
"properties": {
"username": {"title": "Username", "type": "string"},
- "email": {"title": "Email", "type": "string"},
- "full_name": {"title": "Full Name", "type": "string"},
- "disabled": {"title": "Disabled", "type": "boolean"},
+ "email": IsDict(
+ {
+ "title": "Email",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Email", "type": "string"}
+ ),
+ "full_name": IsDict(
+ {
+ "title": "Full Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Full Name", "type": "string"}
+ ),
+ "disabled": IsDict(
+ {
+ "title": "Disabled",
+ "anyOf": [{"type": "boolean"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Disabled", "type": "boolean"}
+ ),
},
},
"Token": {
"required": ["username", "password"],
"type": "object",
"properties": {
- "grant_type": {
- "title": "Grant Type",
- "pattern": "password",
- "type": "string",
- },
+ "grant_type": IsDict(
+ {
+ "title": "Grant Type",
+ "anyOf": [
+ {"pattern": "password", "type": "string"},
+ {"type": "null"},
+ ],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "Grant Type",
+ "pattern": "password",
+ "type": "string",
+ }
+ ),
"username": {"title": "Username", "type": "string"},
"password": {"title": "Password", "type": "string"},
"scope": {"title": "Scope", "type": "string", "default": ""},
- "client_id": {"title": "Client Id", "type": "string"},
- "client_secret": {"title": "Client Secret", "type": "string"},
+ "client_id": IsDict(
+ {
+ "title": "Client Id",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Id", "type": "string"}
+ ),
+ "client_secret": IsDict(
+ {
+ "title": "Client Secret",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Client Secret", "type": "string"}
+ ),
},
},
"ValidationError": {
-from fastapi.testclient import TestClient
from pytest import MonkeyPatch
-from docs_src.settings.app02 import main, test_main
-
-client = TestClient(main.app)
+from ...utils import needs_pydanticv2
+@needs_pydanticv2
def test_settings(monkeypatch: MonkeyPatch):
+ from docs_src.settings.app02 import main
+
monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
settings = main.get_settings()
assert settings.app_name == "Awesome API"
assert settings.items_per_user == 50
+@needs_pydanticv2
def test_override_settings():
+ from docs_src.settings.app02 import test_main
+
test_main.test_app()
--- /dev/null
+from fastapi.testclient import TestClient
+from pytest import MonkeyPatch
+
+from ...utils import needs_pydanticv2
+
+
+@needs_pydanticv2
+def test_settings(monkeypatch: MonkeyPatch):
+ monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
+ from docs_src.settings.tutorial001 import app
+
+ client = TestClient(app)
+ response = client.get("/info")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "app_name": "Awesome API",
+ "admin_email": "admin@example.com",
+ "items_per_user": 50,
+ }
--- /dev/null
+from fastapi.testclient import TestClient
+from pytest import MonkeyPatch
+
+from ...utils import needs_pydanticv1
+
+
+@needs_pydanticv1
+def test_settings(monkeypatch: MonkeyPatch):
+ monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
+ from docs_src.settings.tutorial001_pv1 import app
+
+ client = TestClient(app)
+ response = client.get("/info")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "app_name": "Awesome API",
+ "admin_email": "admin@example.com",
+ "items_per_user": 50,
+ }
from pathlib import Path
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from ...utils import needs_pydanticv1
+
@pytest.fixture(scope="module")
def client(tmp_path_factory: pytest.TempPathFactory):
os.chdir(cwd)
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_create_user(client):
test_user = {"email": "johndoe@example.com", "password": "secret"}
response = client.post("/users/", json=test_user)
assert response.status_code == 400, response.text
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_get_user(client):
response = client.get("/users/1")
assert response.status_code == 200, response.text
assert "id" in data
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_inexistent_user(client):
response = client.get("/users/999")
assert response.status_code == 404, response.text
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_get_users(client):
response = client.get("/users/")
assert response.status_code == 200, response.text
assert "id" in data[0]
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
def test_create_item(client):
item = {"title": "Foo", "description": "Something that fights"}
response = client.post("/users/1/items/", json=item)
assert item_to_check["description"] == item["description"]
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
def test_read_items(client):
response = client.get("/items/")
assert response.status_code == 200, response.text
assert "description" in first_item
-def test_openapi_schema(client):
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
},
},
"Item": {
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"},
+ ),
"id": {"title": "Id", "type": "integer"},
"owner_id": {"title": "Owner Id", "type": "integer"},
},
from pathlib import Path
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
+from ...utils import needs_pydanticv1
+
@pytest.fixture(scope="module")
def client():
test_db.unlink()
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_create_user(client):
test_user = {"email": "johndoe@example.com", "password": "secret"}
response = client.post("/users/", json=test_user)
assert response.status_code == 400, response.text
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_get_user(client):
response = client.get("/users/1")
assert response.status_code == 200, response.text
assert "id" in data
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_inexistent_user(client):
response = client.get("/users/999")
assert response.status_code == 404, response.text
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_get_users(client):
response = client.get("/users/")
assert response.status_code == 200, response.text
assert "id" in data[0]
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
def test_create_item(client):
item = {"title": "Foo", "description": "Something that fights"}
response = client.post("/users/1/items/", json=item)
assert item_to_check["description"] == item["description"]
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
def test_read_items(client):
response = client.get("/items/")
assert response.status_code == 200, response.text
assert "description" in first_item
-def test_openapi_schema(client):
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
},
},
"Item": {
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"},
+ ),
"id": {"title": "Id", "type": "integer"},
"owner_id": {"title": "Owner Id", "type": "integer"},
},
from pathlib import Path
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
-from ...utils import needs_py310
+from ...utils import needs_py310, needs_pydanticv1
@pytest.fixture(scope="module")
@needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_create_user(client):
test_user = {"email": "johndoe@example.com", "password": "secret"}
response = client.post("/users/", json=test_user)
@needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_get_user(client):
response = client.get("/users/1")
assert response.status_code == 200, response.text
@needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_inexistent_user(client):
response = client.get("/users/999")
assert response.status_code == 404, response.text
@needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_get_users(client):
response = client.get("/users/")
assert response.status_code == 200, response.text
@needs_py310
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
def test_create_item(client):
item = {"title": "Foo", "description": "Something that fights"}
response = client.post("/users/1/items/", json=item)
@needs_py310
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
def test_read_items(client):
response = client.get("/items/")
assert response.status_code == 200, response.text
@needs_py310
-def test_openapi_schema(client):
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
},
},
"Item": {
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"},
+ ),
"id": {"title": "Id", "type": "integer"},
"owner_id": {"title": "Owner Id", "type": "integer"},
},
from pathlib import Path
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
-from ...utils import needs_py39
+from ...utils import needs_py39, needs_pydanticv1
@pytest.fixture(scope="module")
@needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_create_user(client):
test_user = {"email": "johndoe@example.com", "password": "secret"}
response = client.post("/users/", json=test_user)
@needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_get_user(client):
response = client.get("/users/1")
assert response.status_code == 200, response.text
@needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_inexistent_user(client):
response = client.get("/users/999")
assert response.status_code == 404, response.text
@needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_get_users(client):
response = client.get("/users/")
assert response.status_code == 200, response.text
@needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_create_item(client):
item = {"title": "Foo", "description": "Something that fights"}
response = client.post("/users/1/items/", json=item)
@needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_read_items(client):
response = client.get("/items/")
assert response.status_code == 200, response.text
@needs_py39
-def test_openapi_schema(client):
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
},
},
"Item": {
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"},
+ ),
"id": {"title": "Id", "type": "integer"},
"owner_id": {"title": "Owner Id", "type": "integer"},
},
from pathlib import Path
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
-from ...utils import needs_py310
+from ...utils import needs_py310, needs_pydanticv1
@pytest.fixture(scope="module", name="client")
@needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_create_user(client):
test_user = {"email": "johndoe@example.com", "password": "secret"}
response = client.post("/users/", json=test_user)
@needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_get_user(client):
response = client.get("/users/1")
assert response.status_code == 200, response.text
@needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_inexistent_user(client):
response = client.get("/users/999")
assert response.status_code == 404, response.text
@needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_get_users(client):
response = client.get("/users/")
assert response.status_code == 200, response.text
@needs_py310
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
def test_create_item(client):
item = {"title": "Foo", "description": "Something that fights"}
response = client.post("/users/1/items/", json=item)
@needs_py310
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
def test_read_items(client):
response = client.get("/items/")
assert response.status_code == 200, response.text
@needs_py310
-def test_openapi_schema(client):
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
},
},
"Item": {
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"},
+ ),
"id": {"title": "Id", "type": "integer"},
"owner_id": {"title": "Owner Id", "type": "integer"},
},
from pathlib import Path
import pytest
+from dirty_equals import IsDict
from fastapi.testclient import TestClient
-from ...utils import needs_py39
+from ...utils import needs_py39, needs_pydanticv1
@pytest.fixture(scope="module", name="client")
@needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_create_user(client):
test_user = {"email": "johndoe@example.com", "password": "secret"}
response = client.post("/users/", json=test_user)
@needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_get_user(client):
response = client.get("/users/1")
assert response.status_code == 200, response.text
@needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_inexistent_user(client):
response = client.get("/users/999")
assert response.status_code == 404, response.text
@needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_get_users(client):
response = client.get("/users/")
assert response.status_code == 200, response.text
@needs_py39
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
def test_create_item(client):
item = {"title": "Foo", "description": "Something that fights"}
response = client.post("/users/1/items/", json=item)
@needs_py39
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
def test_read_items(client):
response = client.get("/items/")
assert response.status_code == 200, response.text
@needs_py39
-def test_openapi_schema(client):
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"}
+ ),
},
},
"Item": {
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"},
- "description": {"title": "Description", "type": "string"},
+ "description": IsDict(
+ {
+ "title": "Description",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Description", "type": "string"},
+ ),
"id": {"title": "Id", "type": "integer"},
"owner_id": {"title": "Owner Id", "type": "integer"},
},
import pytest
+from ...utils import needs_pydanticv1
+
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_testing_dbs(tmp_path_factory: pytest.TempPathFactory):
tmp_path = tmp_path_factory.mktemp("data")
cwd = os.getcwd()
import pytest
-from ...utils import needs_py310
+from ...utils import needs_py310, needs_pydanticv1
@needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_testing_dbs_py39(tmp_path_factory: pytest.TempPathFactory):
tmp_path = tmp_path_factory.mktemp("data")
cwd = os.getcwd()
import pytest
-from ...utils import needs_py39
+from ...utils import needs_py39, needs_pydanticv1
@needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
def test_testing_dbs_py39(tmp_path_factory: pytest.TempPathFactory):
tmp_path = tmp_path_factory.mktemp("data")
cwd = os.getcwd()
import pytest
from fastapi.testclient import TestClient
+from ...utils import needs_pydanticv1
+
@pytest.fixture(scope="module")
def client():
test_db.unlink()
+@needs_pydanticv1
def test_create_user(client):
test_user = {"email": "johndoe@example.com", "password": "secret"}
response = client.post("/users/", json=test_user)
assert response.status_code == 400, response.text
+@needs_pydanticv1
def test_get_user(client):
response = client.get("/users/1")
assert response.status_code == 200, response.text
assert "id" in data
+@needs_pydanticv1
def test_inexistent_user(client):
response = client.get("/users/999")
assert response.status_code == 404, response.text
+@needs_pydanticv1
def test_get_users(client):
response = client.get("/users/")
assert response.status_code == 200, response.text
time.sleep = MagicMock()
+@needs_pydanticv1
def test_get_slowusers(client):
response = client.get("/slowusers/")
assert response.status_code == 200, response.text
assert "id" in data[0]
+@needs_pydanticv1
def test_create_item(client):
item = {"title": "Foo", "description": "Something that fights"}
response = client.post("/users/1/items/", json=item)
assert item_to_check["description"] == item["description"]
+@needs_pydanticv1
def test_read_items(client):
response = client.get("/items/")
assert response.status_code == 200, response.text
assert "description" in first_item
+@needs_pydanticv1
def test_openapi_schema(client):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
from typing import Optional, Union
+from dirty_equals import IsDict
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
"Item": {
"title": "Item",
"type": "object",
- "properties": {"name": {"title": "Name", "type": "string"}},
+ "properties": IsDict(
+ {
+ "name": {
+ "title": "Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"name": {"title": "Name", "type": "string"}}
+ ),
},
"ValidationError": {
"title": "ValidationError",
from typing import Optional, Union
+from dirty_equals import IsDict
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
"Item": {
"title": "Item",
"type": "object",
- "properties": {"name": {"title": "Name", "type": "string"}},
+ "properties": {
+ "name": IsDict(
+ {
+ "title": "Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Name", "type": "string"}
+ )
+ },
},
"ExtendedItem": {
"title": "ExtendedItem",
"required": ["age"],
"type": "object",
"properties": {
- "name": {"title": "Name", "type": "string"},
+ "name": IsDict(
+ {
+ "title": "Name",
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "Name", "type": "string"}
+ ),
"age": {"title": "Age", "type": "integer"},
},
},
import pytest
from fastapi import FastAPI
+from fastapi.exceptions import ResponseValidationError
from fastapi.testclient import TestClient
-from pydantic import BaseModel, ValidationError
+from pydantic import BaseModel
app = FastAPI()
def test_invalid():
- with pytest.raises(ValidationError):
+ with pytest.raises(ResponseValidationError):
client.get("/items/invalid")
def test_invalid_none():
- with pytest.raises(ValidationError):
+ with pytest.raises(ResponseValidationError):
client.get("/items/invalidnone")
def test_double_invalid():
- with pytest.raises(ValidationError):
+ with pytest.raises(ResponseValidationError):
client.get("/items/innerinvalid")
def test_invalid_list():
- with pytest.raises(ValidationError):
+ with pytest.raises(ResponseValidationError):
client.get("/items/invalidlist")
import pytest
from fastapi import FastAPI
+from fastapi.exceptions import ResponseValidationError
from fastapi.testclient import TestClient
-from pydantic import ValidationError
from pydantic.dataclasses import dataclass
app = FastAPI()
def test_invalid():
- with pytest.raises(ValidationError):
+ with pytest.raises(ResponseValidationError):
client.get("/items/invalid")
def test_double_invalid():
- with pytest.raises(ValidationError):
+ with pytest.raises(ResponseValidationError):
client.get("/items/innerinvalid")
def test_invalid_list():
- with pytest.raises(ValidationError):
+ with pytest.raises(ResponseValidationError):
client.get("/items/invalidlist")
from typing import List
from fastapi import FastAPI
-from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
}
],
}
-
-
-client = TestClient(app)
-
-
-def test_recursive():
- response = client.get("/items/recursive")
- assert response.status_code == 200, response.text
- assert response.json() == {
- "sub_items": [{"name": "subitem", "sub_items": []}],
- "name": "item",
- }
-
- response = client.get("/items/recursive-submodel")
- assert response.status_code == 200, response.text
- assert response.json() == {
- "name": "item",
- "sub_items1": [
- {
- "name": "subitem",
- "sub_items2": [
- {
- "name": "subsubitem",
- "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}],
- }
- ],
- }
- ],
- }
--- /dev/null
+from typing import List
+
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class RecursiveItem(BaseModel):
+ sub_items: List["RecursiveItem"] = []
+ name: str
+
+
+RecursiveItem.model_rebuild()
+
+
+class RecursiveSubitemInSubmodel(BaseModel):
+ sub_items2: List["RecursiveItemViaSubmodel"] = []
+ name: str
+
+
+class RecursiveItemViaSubmodel(BaseModel):
+ sub_items1: List[RecursiveSubitemInSubmodel] = []
+ name: str
+
+
+RecursiveSubitemInSubmodel.model_rebuild()
+RecursiveItemViaSubmodel.model_rebuild()
+
+
+@app.get("/items/recursive", response_model=RecursiveItem)
+def get_recursive():
+ return {"name": "item", "sub_items": [{"name": "subitem", "sub_items": []}]}
+
+
+@app.get("/items/recursive-submodel", response_model=RecursiveItemViaSubmodel)
+def get_recursive_submodel():
+ return {
+ "name": "item",
+ "sub_items1": [
+ {
+ "name": "subitem",
+ "sub_items2": [
+ {
+ "name": "subsubitem",
+ "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}],
+ }
+ ],
+ }
+ ],
+ }
--- /dev/null
+from fastapi.testclient import TestClient
+
+from ..utils import needs_pydanticv1
+
+
+@needs_pydanticv1
+def test_recursive():
+ from .app_pv1 import app
+
+ client = TestClient(app)
+ response = client.get("/items/recursive")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "sub_items": [{"name": "subitem", "sub_items": []}],
+ "name": "item",
+ }
+
+ response = client.get("/items/recursive-submodel")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "name": "item",
+ "sub_items1": [
+ {
+ "name": "subitem",
+ "sub_items2": [
+ {
+ "name": "subsubitem",
+ "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}],
+ }
+ ],
+ }
+ ],
+ }
--- /dev/null
+from fastapi.testclient import TestClient
+
+from ..utils import needs_pydanticv2
+
+
+@needs_pydanticv2
+def test_recursive():
+ from .app_pv2 import app
+
+ client = TestClient(app)
+ response = client.get("/items/recursive")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "sub_items": [{"name": "subitem", "sub_items": []}],
+ "name": "item",
+ }
+
+ response = client.get("/items/recursive-submodel")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "name": "item",
+ "sub_items1": [
+ {
+ "name": "subitem",
+ "sub_items2": [
+ {
+ "name": "subsubitem",
+ "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}],
+ }
+ ],
+ }
+ ],
+ }
import sys
import pytest
+from fastapi._compat import PYDANTIC_V2
needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9+")
needs_py310 = pytest.mark.skipif(
sys.version_info < (3, 10), reason="requires python3.10+"
)
+needs_pydanticv2 = pytest.mark.skipif(not PYDANTIC_V2, reason="requires Pydantic v2")
+needs_pydanticv1 = pytest.mark.skipif(PYDANTIC_V2, reason="requires Pydantic v1")