mirror of https://github.com/tiangolo/fastapi.git
♻️ Deprecate parameter `regex`, use `pattern` instead (#9786)
* 📝 Update docs to deprecate regex, recommend pattern * ♻️ Update examples to use new pattern instead of regex * 📝 Add new example with deprecated regex * ♻️ Add deprecation notes and warnings for regex * ✅ Add tests for regex deprecation * ✅ Update tests for compatibility with Pydantic v1
This commit is contained in:
parent
9889182d50
commit
b892664f25
|
|
@ -277,7 +277,7 @@ You can also add a parameter `min_length`:
|
|||
|
||||
## 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+"
|
||||
|
||||
|
|
@ -315,7 +315,7 @@ You can define a <abbr title="A regular expression, regex or regexp is a sequenc
|
|||
{!> ../../../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`.
|
||||
|
|
@ -325,6 +325,20 @@ If you feel lost with all these **"regular expression"** ideas, don't worry. The
|
|||
|
||||
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`.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ app = FastAPI()
|
|||
@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"}]}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ app = FastAPI()
|
|||
@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"}]}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ app = FastAPI()
|
|||
@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"}]}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
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
|
||||
|
|
@ -8,7 +8,7 @@ app = FastAPI()
|
|||
@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"}]}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ app = FastAPI()
|
|||
@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:
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ async def read_items(
|
|||
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,
|
||||
)
|
||||
):
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ async def read_items(
|
|||
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
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ async def read_items(
|
|||
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
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ async def read_items(
|
|||
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
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ async def read_items(
|
|||
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,
|
||||
)
|
||||
):
|
||||
|
|
|
|||
|
|
@ -18,7 +18,12 @@ def Path( # noqa: N802
|
|||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -65,7 +70,12 @@ def Query( # noqa: N802
|
|||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -113,7 +123,12 @@ def Header( # noqa: N802
|
|||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -161,7 +176,12 @@ def Cookie( # noqa: N802
|
|||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -210,7 +230,12 @@ def Body( # noqa: N802
|
|||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -256,7 +281,12 @@ def Form( # noqa: N802
|
|||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -301,7 +331,12 @@ def File( # noqa: N802
|
|||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
|
|||
|
|
@ -33,7 +33,12 @@ class Param(FieldInfo):
|
|||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -51,7 +56,7 @@ class Param(FieldInfo):
|
|||
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
|
||||
|
|
@ -70,11 +75,17 @@ class Param(FieldInfo):
|
|||
)
|
||||
if examples is not None:
|
||||
kwargs["examples"] = examples
|
||||
if regex is not None:
|
||||
print(f"regex: {regex}")
|
||||
warnings.warn(
|
||||
"`regex` has been depreacated, please use `pattern` instead",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=4,
|
||||
)
|
||||
if PYDANTIC_V2:
|
||||
kwargs["annotation"] = annotation
|
||||
kwargs["pattern"] = pattern or regex
|
||||
else:
|
||||
# TODO: pv2 figure out how to deprecate regex
|
||||
kwargs["regex"] = pattern or regex
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
|
@ -101,7 +112,12 @@ class Path(Param):
|
|||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -156,7 +172,12 @@ class Query(Param):
|
|||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -210,7 +231,12 @@ class Header(Param):
|
|||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -264,7 +290,12 @@ class Cookie(Param):
|
|||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -317,7 +348,12 @@ class Body(FieldInfo):
|
|||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -334,7 +370,7 @@ class Body(FieldInfo):
|
|||
warnings.warn(
|
||||
"`example` has been depreacated, please use `examples` instead",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=1,
|
||||
stacklevel=4,
|
||||
)
|
||||
self.example = example
|
||||
kwargs = dict(
|
||||
|
|
@ -352,11 +388,16 @@ class Body(FieldInfo):
|
|||
)
|
||||
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,
|
||||
)
|
||||
if PYDANTIC_V2:
|
||||
kwargs["annotation"] = annotation
|
||||
kwargs["pattern"] = pattern or regex
|
||||
else:
|
||||
# TODO: pv2 figure out how to deprecate regex
|
||||
kwargs["regex"] = pattern or regex
|
||||
super().__init__(
|
||||
**kwargs,
|
||||
|
|
@ -383,7 +424,12 @@ class Form(Body):
|
|||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
@ -433,7 +479,12 @@ class File(Form):
|
|||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,183 @@
|
|||
from typing import Annotated
|
||||
|
||||
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 .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",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
from typing import Annotated
|
||||
|
||||
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 .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",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
Loading…
Reference in New Issue