Skip to content

Commit

Permalink
Support Quart 0.19 onwards
Browse files Browse the repository at this point in the history
Quart 0.19 adopts Flask's stricter make_response code, hence the
change here (either a value or tuple).

Also the typing has improved.
  • Loading branch information
pgjones committed Nov 12, 2023
1 parent 2365968 commit bbf561d
Show file tree
Hide file tree
Showing 6 changed files with 33 additions and 17 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ pydata_sphinx_theme = { version = "*", optional = true }
pyhumps = ">=1.6.1"
python = ">=3.8"
pydantic=">=2"
quart = ">=0.18.1"
quart = ">=0.19.0"
typing_extensions = { version = "*", python = "<3.9" }

[tool.poetry.dev-dependencies]
Expand Down
17 changes: 10 additions & 7 deletions src/quart_schema/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from dataclasses import asdict, is_dataclass
from functools import wraps
from types import new_class
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
from typing import Any, Callable, cast, Dict, Iterable, List, Optional, Tuple, Union

import click
from humps import camelize
Expand All @@ -16,6 +16,7 @@
from quart import current_app, Quart, render_template_string, Response, ResponseReturnValue
from quart.cli import pass_script_info, ScriptInfo
from quart.json.provider import DefaultJSONProvider
from quart.typing import ResponseValue
from werkzeug.routing.converters import NumberConverter
from werkzeug.routing.rules import Rule

Expand Down Expand Up @@ -335,16 +336,15 @@ def _split_convert_definitions(schema: dict, convert_casing: bool) -> Tuple[dict
def convert_model_result(func: Callable) -> Callable:
@wraps(func)
async def decorator(result: ResponseReturnValue) -> Response:
status_or_headers = None
headers = None
if isinstance(result, tuple):
value, status_or_headers, headers = result + (None,) * (3 - len(result))
value = result[0]
else:
value = result

was_model = False
dict_or_value: ResponseValue
if is_dataclass(value):
dict_or_value = asdict(value)
dict_or_value = asdict(value) # type: ignore
was_model = True
elif isinstance(value, BaseModel):
dict_or_value = value.model_dump(by_alias=current_app.config["QUART_SCHEMA_BY_ALIAS"])
Expand All @@ -353,9 +353,12 @@ async def decorator(result: ResponseReturnValue) -> Response:
dict_or_value = value

if was_model and current_app.config["QUART_SCHEMA_CONVERT_CASING"]:
dict_or_value = camelize(dict_or_value)
dict_or_value = camelize(cast(Dict[str, Any], dict_or_value))

return await func((dict_or_value, status_or_headers, headers))
if isinstance(result, tuple):
return await func((dict_or_value, *result[1:]))
else:
return await func(dict_or_value)

return decorator

Expand Down
2 changes: 1 addition & 1 deletion src/quart_schema/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ async def wrapper(*args: Any, **kwargs: Any) -> Any:
else:
return result

return wrapper
return wrapper # type: ignore

return decorator

Expand Down
21 changes: 17 additions & 4 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import pytest
from pydantic import BaseModel
from pydantic.dataclasses import dataclass as pydantic_dataclass
from quart import Quart
from quart import Quart, ResponseReturnValue

from quart_schema import QuartSchema, ResponseReturnValue
from quart_schema import QuartSchema
from quart_schema.typing import PydanticModel


Expand Down Expand Up @@ -36,7 +36,20 @@ async def test_make_response(type_: PydanticModel) -> None:

@app.route("/")
async def index() -> ResponseReturnValue:
return type_(name="bob", age=2)
return type_(name="bob", age=2) # type: ignore

test_client = app.test_client()
response = await test_client.get("/")
assert (await response.get_json()) == {"name": "bob", "age": 2}


async def test_make_response_no_model() -> None:
app = Quart(__name__)
QuartSchema(app)

@app.route("/")
async def index() -> ResponseReturnValue:
return {"name": "bob", "age": 2}, {"Content-Type": "application/json"}

test_client = app.test_client()
response = await test_client.get("/")
Expand All @@ -52,7 +65,7 @@ async def test_make_pydantic_encoder_response() -> None:
app = Quart(__name__)
QuartSchema(app)

@app.route("/")
@app.route("/") # type: ignore
async def index() -> PydanticEncoded:
return PydanticEncoded(a=UUID("23ef2e02-1c20-49de-b05e-e9fe2431c474"), b=Path("/"))

Expand Down
4 changes: 2 additions & 2 deletions tests/test_casing.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ async def test_response_casing() -> None:

@app.route("/", methods=["GET"])
@validate_response(Data)
async def index() -> ResponseReturnValue:
async def index() -> Data:
return Data(snake_case="Hello")

test_client = app.test_client()
response = await test_client.get("/")
assert await response.get_data(as_text=True) == '{"snakeCase":"Hello"}'
assert await response.get_data(as_text=True) == '{"snakeCase":"Hello"}\n'


@dataclass
Expand Down
4 changes: 2 additions & 2 deletions tests/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ async def test_send_form(type_: PydanticModel) -> None:

@app.route("/", methods=["POST"])
@validate_request(type_, source=DataSource.FORM)
async def index(data: PydanticModel) -> ResponseReturnValue:
async def index(data: PydanticModel) -> PydanticModel:
return data

test_client = app.test_client()
Expand All @@ -58,7 +58,7 @@ async def test_hypothesis_dataclass(data: DCDetails) -> None:

@app.route("/", methods=["POST"])
@validate_request(DCDetails)
async def index(data: DCDetails) -> ResponseReturnValue:
async def index(data: DCDetails) -> DCDetails:
return data

test_client = app.test_client()
Expand Down

0 comments on commit bbf561d

Please sign in to comment.