Browse Source

Pass None instead of the default value to parameters that accept it when

null is given

Signed-off-by: merlinz01 <[email protected]>
pull/12135/head
merlinz01 2 years ago
parent
commit
a77b4d72b1
  1. 36
      fastapi/dependencies/utils.py
  2. 44
      tests/test_none_passed_when_null_received.py

36
fastapi/dependencies/utils.py

@ -1,6 +1,7 @@
import dataclasses import dataclasses
import inspect import inspect
import sys import sys
import types
from collections.abc import ( from collections.abc import (
AsyncGenerator, AsyncGenerator,
AsyncIterable, AsyncIterable,
@ -31,6 +32,7 @@ from fastapi._compat import (
ModelField, ModelField,
RequiredParam, RequiredParam,
Undefined, Undefined,
UndefinedType,
copy_field_info, copy_field_info,
create_body_model, create_body_model,
evaluate_forwardref, evaluate_forwardref,
@ -596,7 +598,7 @@ async def solve_dependencies(
*, *,
request: Request | WebSocket, request: Request | WebSocket,
dependant: Dependant, dependant: Dependant,
body: dict[str, Any] | FormData | None = None, body: dict[str, Any] | FormData | UndefinedType | None = None,
background_tasks: StarletteBackgroundTasks | None = None, background_tasks: StarletteBackgroundTasks | None = None,
response: Response | None = None, response: Response | None = None,
dependency_overrides_provider: Any | None = None, dependency_overrides_provider: Any | None = None,
@ -732,10 +734,24 @@ async def solve_dependencies(
) )
def _allows_none(field: ModelField) -> bool:
origin = get_origin(field.type_)
return (origin is Union or origin is types.UnionType) and type(None) in get_args(
field.type_
)
def _validate_value_with_model_field( def _validate_value_with_model_field(
*, field: ModelField, value: Any, values: dict[str, Any], loc: tuple[str, ...] *, field: ModelField, value: Any, values: dict[str, Any], loc: tuple[str, ...]
) -> tuple[Any, list[Any]]: ) -> tuple[Any, list[Any]]:
if value is Undefined:
if field.required:
return None, [get_missing_field_error(loc=loc)]
else:
return deepcopy(field.default), []
if value is None: if value is None:
if _allows_none(field):
return value, []
if field.field_info.is_required(): if field.field_info.is_required():
return None, [get_missing_field_error(loc=loc)] return None, [get_missing_field_error(loc=loc)]
else: else:
@ -758,9 +774,9 @@ def _get_multidict_value(
): ):
value = values.getlist(alias) value = values.getlist(alias)
else: else:
value = values.get(alias, None) value = values.get(alias, Undefined)
if ( if (
value is None value is Undefined
or ( or (
isinstance(field.field_info, params.Form) isinstance(field.field_info, params.Form)
and isinstance(value, str) # For type checks and isinstance(value, str) # For type checks
@ -772,7 +788,7 @@ def _get_multidict_value(
) )
): ):
if field.field_info.is_required(): if field.field_info.is_required():
return return Undefined
else: else:
return deepcopy(field.default) return deepcopy(field.default)
return value return value
@ -932,7 +948,7 @@ async def _extract_form_body(
for sub_value in value: for sub_value in value:
results.append(await sub_value.read()) results.append(await sub_value.read())
value = serialize_sequence_value(field=field, value=results) value = serialize_sequence_value(field=field, value=results)
if value is not None: if value is not Undefined and value is not None:
values[get_validation_alias(field)] = value values[get_validation_alias(field)] = value
field_aliases = {get_validation_alias(field) for field in body_fields} field_aliases = {get_validation_alias(field) for field in body_fields}
for key in received_body.keys(): for key in received_body.keys():
@ -947,7 +963,7 @@ async def _extract_form_body(
async def request_body_to_args( async def request_body_to_args(
body_fields: list[ModelField], body_fields: list[ModelField],
received_body: dict[str, Any] | FormData | None, received_body: dict[str, Any] | FormData | UndefinedType | None,
embed_body_fields: bool, embed_body_fields: bool,
) -> tuple[dict[str, Any], list[dict[str, Any]]]: ) -> tuple[dict[str, Any], list[dict[str, Any]]]:
values: dict[str, Any] = {} values: dict[str, Any] = {}
@ -977,10 +993,12 @@ async def request_body_to_args(
return {first_field.name: v_}, errors_ return {first_field.name: v_}, errors_
for field in body_fields: for field in body_fields:
loc = ("body", get_validation_alias(field)) loc = ("body", get_validation_alias(field))
value: Any | None = None value: Any | None = Undefined
if body_to_process is not None: if body_to_process is not None and not isinstance(
body_to_process, UndefinedType
):
try: try:
value = body_to_process.get(get_validation_alias(field)) value = body_to_process.get(get_validation_alias(field), Undefined)
# If the received body is a list, not a dict # If the received body is a list, not a dict
except AttributeError: except AttributeError:
errors.append(get_missing_field_error(loc)) errors.append(get_missing_field_error(loc))

44
tests/test_none_passed_when_null_received.py

@ -0,0 +1,44 @@
from typing import Annotated, Optional, Union
import pytest
from fastapi import Body, FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
DEFAULT = 1234567890
@app.post("/api1")
def api1(integer_or_null: Annotated[int | None, Body(embed=True)] = DEFAULT) -> dict:
return {"received": integer_or_null}
@app.post("/api2")
def api2(integer_or_null: Annotated[Optional[int], Body(embed=True)] = DEFAULT) -> dict:
return {"received": integer_or_null}
@app.post("/api3")
def api3(
integer_or_null: Annotated[Union[int, None], Body(embed=True)] = DEFAULT,
) -> dict:
return {"received": integer_or_null}
@app.post("/api4")
def api4(integer_or_null: Optional[int] = Body(embed=True, default=DEFAULT)) -> dict:
return {"received": integer_or_null}
client = TestClient(app)
@pytest.mark.parametrize("api", ["/api1", "/api2", "/api3", "/api4"])
def test_api1_integer(api):
response = client.post(api, json={"integer_or_null": 100})
assert response.status_code == 200, response.text
assert response.json() == {"received": 100}
response = client.post(api, json={"integer_or_null": None})
assert response.status_code == 200, response.text
assert response.json() == {"received": None}
Loading…
Cancel
Save