Browse Source

Merge branch 'master' into fix-duplicate-special-dependency-handling

pull/12406/head
Peter Volf 7 months ago
committed by GitHub
parent
commit
398c418363
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 13
      docs/en/docs/release-notes.md
  2. 3
      docs_src/query_params_str_validations/tutorial006d.py
  3. 3
      docs_src/query_params_str_validations/tutorial006d_an.py
  4. 3
      docs_src/query_params_str_validations/tutorial006d_an_py39.py
  5. 2
      fastapi/__init__.py
  6. 9
      fastapi/_compat.py
  7. 14
      fastapi/dependencies/utils.py
  8. 4
      fastapi/routing.py
  9. 2
      pyproject.toml
  10. 25
      tests/test_computed_fields.py
  11. 36
      tests/test_openapi_separate_input_output_schemas.py

13
docs/en/docs/release-notes.md

@ -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).

3
docs_src/query_params_str_validations/tutorial006d.py

@ -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})

3
docs_src/query_params_str_validations/tutorial006d_an.py

@ -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})

3
docs_src/query_params_str_validations/tutorial006d_an_py39.py

@ -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})

2
fastapi/__init__.py

@ -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

9
fastapi/_compat.py

@ -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,
)

14
fastapi/dependencies/utils.py

@ -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:

4
fastapi/routing.py

@ -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

2
pyproject.toml

@ -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",
]

25
tests/test_computed_fields.py

@ -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,8 +63,24 @@ 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": {
"Rectangle": {

36
tests/test_openapi_separate_input_output_schemas.py

@ -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…
Cancel
Save