mirror of https://github.com/tiangolo/fastapi.git
Merge branch 'master' into fix-duplicate-special-dependency-handling
This commit is contained in:
commit
398c418363
|
|
@ -7,6 +7,19 @@ hide:
|
|||
|
||||
## Latest Changes
|
||||
|
||||
## 0.115.2
|
||||
|
||||
### Upgrades
|
||||
|
||||
* ⬆️ Upgrade Starlette to `>=0.37.2,<0.41.0`. PR [#12431](https://github.com/fastapi/fastapi/pull/12431) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.115.1
|
||||
|
||||
### Fixes
|
||||
|
||||
* 🐛 Fix openapi generation with responses kwarg. PR [#10895](https://github.com/fastapi/fastapi/pull/10895) by [@flxdot](https://github.com/flxdot).
|
||||
* 🐛 Remove `Required` shadowing from fastapi using Pydantic v2. PR [#12197](https://github.com/fastapi/fastapi/pull/12197) by [@pachewise](https://github.com/pachewise).
|
||||
|
||||
### Refactors
|
||||
|
||||
* ♻️ Update type annotations for improved `python-multipart`. PR [#12407](https://github.com/fastapi/fastapi/pull/12407) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
from fastapi import FastAPI, Query
|
||||
from pydantic import Required
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items(q: str = Query(default=Required, min_length=3)):
|
||||
async def read_items(q: str = Query(default=..., min_length=3)):
|
||||
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
|
||||
if q:
|
||||
results.update({"q": q})
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
from fastapi import FastAPI, Query
|
||||
from pydantic import Required
|
||||
from typing_extensions import Annotated
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items(q: Annotated[str, Query(min_length=3)] = Required):
|
||||
async def read_items(q: Annotated[str, Query(min_length=3)] = ...):
|
||||
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
|
||||
if q:
|
||||
results.update({"q": q})
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
from typing import Annotated
|
||||
|
||||
from fastapi import FastAPI, Query
|
||||
from pydantic import Required
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items(q: Annotated[str, Query(min_length=3)] = Required):
|
||||
async def read_items(q: Annotated[str, Query(min_length=3)] = ...):
|
||||
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
|
||||
if q:
|
||||
results.update({"q": q})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.115.0"
|
||||
__version__ = "0.115.2"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ if PYDANTIC_V2:
|
|||
general_plain_validator_function as with_info_plain_validator_function, # noqa: F401
|
||||
)
|
||||
|
||||
Required = PydanticUndefined
|
||||
RequiredParam = PydanticUndefined
|
||||
Undefined = PydanticUndefined
|
||||
UndefinedType = PydanticUndefinedType
|
||||
evaluate_forwardref = eval_type_lenient
|
||||
|
|
@ -313,9 +313,10 @@ else:
|
|||
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
|
||||
)
|
||||
|
||||
# Keeping old "Required" functionality from Pydantic V1, without
|
||||
# shadowing typing.Required.
|
||||
RequiredParam: Any = Ellipsis # type: ignore[no-redef]
|
||||
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
|
||||
Undefined as Undefined,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ from fastapi._compat import (
|
|||
PYDANTIC_V2,
|
||||
ErrorWrapper,
|
||||
ModelField,
|
||||
Required,
|
||||
RequiredParam,
|
||||
Undefined,
|
||||
_regenerate_error_with_loc,
|
||||
copy_field_info,
|
||||
|
|
@ -377,7 +377,9 @@ def analyze_param(
|
|||
field_info = copy_field_info(
|
||||
field_info=fastapi_annotation, annotation=use_annotation
|
||||
)
|
||||
assert field_info.default is Undefined or field_info.default is Required, (
|
||||
assert (
|
||||
field_info.default is Undefined or field_info.default is RequiredParam
|
||||
), (
|
||||
f"`{field_info.__class__.__name__}` default value cannot be set in"
|
||||
f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
|
||||
)
|
||||
|
|
@ -385,7 +387,7 @@ def analyze_param(
|
|||
assert not is_path_param, "Path parameters cannot have default values"
|
||||
field_info.default = value
|
||||
else:
|
||||
field_info.default = Required
|
||||
field_info.default = RequiredParam
|
||||
# Get Annotated Depends
|
||||
elif isinstance(fastapi_annotation, params.Depends):
|
||||
depends = fastapi_annotation
|
||||
|
|
@ -434,9 +436,9 @@ def analyze_param(
|
|||
), f"Cannot specify FastAPI annotation for type {type_annotation!r}"
|
||||
# Handle default assignations, neither field_info nor depends was not found in Annotated nor default value
|
||||
elif field_info is None and depends is None:
|
||||
default_value = value if value is not inspect.Signature.empty else Required
|
||||
default_value = value if value is not inspect.Signature.empty else RequiredParam
|
||||
if is_path_param:
|
||||
# We might check here that `default_value is Required`, but the fact is that the same
|
||||
# We might check here that `default_value is RequiredParam`, 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(annotation=use_annotation)
|
||||
|
|
@ -480,7 +482,7 @@ def analyze_param(
|
|||
type_=use_annotation_from_field_info,
|
||||
default=field_info.default,
|
||||
alias=alias,
|
||||
required=field_info.default in (Required, Undefined),
|
||||
required=field_info.default in (RequiredParam, Undefined),
|
||||
field_info=field_info,
|
||||
)
|
||||
if is_path_param:
|
||||
|
|
|
|||
|
|
@ -541,7 +541,9 @@ class APIRoute(routing.Route):
|
|||
additional_status_code
|
||||
), f"Status code {additional_status_code} must not have a response body"
|
||||
response_name = f"Response_{additional_status_code}_{self.unique_id}"
|
||||
response_field = create_model_field(name=response_name, type_=model)
|
||||
response_field = create_model_field(
|
||||
name=response_name, type_=model, mode="serialization"
|
||||
)
|
||||
response_fields[additional_status_code] = response_field
|
||||
if response_fields:
|
||||
self.response_fields: Dict[Union[int, str], ModelField] = response_fields
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ classifiers = [
|
|||
"Topic :: Internet :: WWW/HTTP",
|
||||
]
|
||||
dependencies = [
|
||||
"starlette>=0.37.2,<0.39.0",
|
||||
"starlette>=0.37.2,<0.41.0",
|
||||
"pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0",
|
||||
"typing-extensions>=4.8.0",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -24,13 +24,18 @@ def get_client():
|
|||
def read_root() -> Rectangle:
|
||||
return Rectangle(width=3, length=4)
|
||||
|
||||
@app.get("/responses", responses={200: {"model": Rectangle}})
|
||||
def read_responses() -> Rectangle:
|
||||
return Rectangle(width=3, length=4)
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", ["/", "/responses"])
|
||||
@needs_pydanticv2
|
||||
def test_get(client: TestClient):
|
||||
response = client.get("/")
|
||||
def test_get(client: TestClient, path: str):
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"width": 3, "length": 4, "area": 12}
|
||||
|
||||
|
|
@ -58,7 +63,23 @@ def test_openapi_schema(client: TestClient):
|
|||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"/responses": {
|
||||
"get": {
|
||||
"summary": "Read Responses",
|
||||
"operationId": "read_responses_responses_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Rectangle"}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ class Item(BaseModel):
|
|||
def get_app_client(separate_input_output_schemas: bool = True) -> TestClient:
|
||||
app = FastAPI(separate_input_output_schemas=separate_input_output_schemas)
|
||||
|
||||
@app.post("/items/")
|
||||
def create_item(item: Item):
|
||||
@app.post("/items/", responses={402: {"model": Item}})
|
||||
def create_item(item: Item) -> Item:
|
||||
return item
|
||||
|
||||
@app.post("/items-list/")
|
||||
|
|
@ -174,7 +174,23 @@ def test_openapi_schema():
|
|||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Item-Output"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"402": {
|
||||
"description": "Payment Required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Item-Output"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
|
|
@ -374,7 +390,19 @@ def test_openapi_schema_no_separate():
|
|||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
}
|
||||
},
|
||||
},
|
||||
"402": {
|
||||
"description": "Payment Required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
|
|
|
|||
Loading…
Reference in New Issue