From 2521821885d6030fe2361b99a2f6986f7de7724e Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 23 Dec 2024 15:25:58 +0100 Subject: [PATCH 1/7] test cases --- tests/test_edgecases_responses.py | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/test_edgecases_responses.py diff --git a/tests/test_edgecases_responses.py b/tests/test_edgecases_responses.py new file mode 100644 index 00000000..a1399b4b --- /dev/null +++ b/tests/test_edgecases_responses.py @@ -0,0 +1,50 @@ +import os + +from esmerald import Esmerald, Redirect, Request, Template +from esmerald.config.template import TemplateConfig +from esmerald.responses.base import RedirectResponse +from esmerald.routing.gateways import Gateway +from esmerald.routing.handlers import get +from esmerald.testclient import EsmeraldTestClient + + +def test_issue1(template_dir, test_client_factory): + path = os.path.join(template_dir, "start.html") + with open(path, "w") as file: + file.write("Hello, world") + + @get() + async def start(request: Request) -> Template: + return Redirect(path="/home", status_code=301) + + app = Esmerald( + debug=True, + routes=[Gateway("/", handler=start)], + template_config=TemplateConfig( + directory=template_dir, + ), + ) + client = EsmeraldTestClient(app) + response = client.get("/") + assert response.status_code == 301 + + +def test_issue2(template_dir, test_client_factory): + path = os.path.join(template_dir, "start.html") + with open(path, "w") as file: + file.write("Hello, world") + + @get() + async def start(request: Request) -> Template: + return RedirectResponse(url="/home", status_code=301) + + app = Esmerald( + debug=True, + routes=[Gateway("/", handler=start)], + template_config=TemplateConfig( + directory=template_dir, + ), + ) + client = EsmeraldTestClient(app) + response = client.get("/") + assert response.status_code == 301 From 771cde437dec989d6d941df465be39664589114e Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 23 Dec 2024 23:06:24 +0100 Subject: [PATCH 2/7] Changes: - rename test methods - add test for route with GET and POST --- tests/test_edgecases_responses.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/tests/test_edgecases_responses.py b/tests/test_edgecases_responses.py index a1399b4b..8488aa31 100644 --- a/tests/test_edgecases_responses.py +++ b/tests/test_edgecases_responses.py @@ -1,14 +1,20 @@ import os -from esmerald import Esmerald, Redirect, Request, Template +from pydantic import BaseModel + +from esmerald import Esmerald, Form, Redirect, Request, Template from esmerald.config.template import TemplateConfig from esmerald.responses.base import RedirectResponse from esmerald.routing.gateways import Gateway -from esmerald.routing.handlers import get +from esmerald.routing.handlers import get, route from esmerald.testclient import EsmeraldTestClient -def test_issue1(template_dir, test_client_factory): +class Model(BaseModel): + id: str + + +def test_return_response_container(template_dir): path = os.path.join(template_dir, "start.html") with open(path, "w") as file: file.write("Hello, world") @@ -23,13 +29,14 @@ async def start(request: Request) -> Template: template_config=TemplateConfig( directory=template_dir, ), + middleware=[], ) client = EsmeraldTestClient(app) response = client.get("/") assert response.status_code == 301 -def test_issue2(template_dir, test_client_factory): +def test_return_response(template_dir): path = os.path.join(template_dir, "start.html") with open(path, "w") as file: file.write("Hello, world") @@ -48,3 +55,17 @@ async def start(request: Request) -> Template: client = EsmeraldTestClient(app) response = client.get("/") assert response.status_code == 301 + + +def test_get_and_post(): + @route(methods=["GET", "POST"]) + async def start(request: Request, data: Model | None = Form()) -> bytes: + return b"hello world" + + app = Esmerald( + debug=True, + routes=[Gateway("/", handler=start)], + ) + client = EsmeraldTestClient(app) + response = client.get("/") + assert response.status_code == 200 From 07af7a4d6f3c88daa078a7d8942528c9fab4a854 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 23 Dec 2024 23:10:27 +0100 Subject: [PATCH 3/7] rename to edgecases --- tests/{test_edgecases_responses.py => test_edgecases.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_edgecases_responses.py => test_edgecases.py} (100%) diff --git a/tests/test_edgecases_responses.py b/tests/test_edgecases.py similarity index 100% rename from tests/test_edgecases_responses.py rename to tests/test_edgecases.py From ae00bfdf0c5a561658e838c837ce23e6c8ebf385 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 24 Dec 2024 00:04:16 +0100 Subject: [PATCH 4/7] add py39 compatibility --- tests/test_edgecases.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_edgecases.py b/tests/test_edgecases.py index 8488aa31..4e4398e5 100644 --- a/tests/test_edgecases.py +++ b/tests/test_edgecases.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from pydantic import BaseModel From 5b085901daf780d9c2b476b6d24049e14e0277a0 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 17 Jan 2025 13:22:11 +0100 Subject: [PATCH 5/7] CHanges: - bypass ResponseContainer when LilyaResponse was found - Update edge cases Note: there was a trick: forward strings aren't resolved correctly but removing from __future__ helped --- esmerald/routing/base.py | 2 ++ esmerald/routing/router.py | 14 ++++++++------ tests/test_edgecases.py | 9 ++++----- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/esmerald/routing/base.py b/esmerald/routing/base.py index c5da8874..8826a8d3 100644 --- a/esmerald/routing/base.py +++ b/esmerald/routing/base.py @@ -523,6 +523,8 @@ async def get_response_for_request( parameter_model=parameter_model, request=request, ) + if isinstance(response_data, LilyaResponse): + return response_data response = await self.to_response( app=scope["app"], diff --git a/esmerald/routing/router.py b/esmerald/routing/router.py index 1d30bcb8..b1d8ff2c 100644 --- a/esmerald/routing/router.py +++ b/esmerald/routing/router.py @@ -481,9 +481,9 @@ async def another(request: Request) -> str: path = "/" else: assert path.startswith("/"), "A path prefix must start with '/'" - assert not path.endswith( - "/" - ), "A path must not end with '/', as the routes will start with '/'" + assert not path.endswith("/"), ( + "A path must not end with '/', as the routes will start with '/'" + ) new_routes: list[Any] = [] for route in routes or []: @@ -511,9 +511,9 @@ async def another(request: Request) -> str: ) new_routes.append(route) - assert lifespan is None or ( - on_startup is None and on_shutdown is None - ), "Use either 'lifespan' or 'on_startup'/'on_shutdown', not both." + assert lifespan is None or (on_startup is None and on_shutdown is None), ( + "Use either 'lifespan' or 'on_startup'/'on_shutdown', not both." + ) super().__init__( redirect_slashes=redirect_slashes, @@ -2270,6 +2270,8 @@ def validate_handler(self) -> None: self.validate_reserved_kwargs() async def to_response(self, app: "Esmerald", data: Any) -> LilyaResponse: + if isinstance(data, LilyaResponse): + return data response_handler = self.get_response_for_handler() return await response_handler(app=app, data=data) # type: ignore[call-arg] diff --git a/tests/test_edgecases.py b/tests/test_edgecases.py index 4e4398e5..bf5ab8c4 100644 --- a/tests/test_edgecases.py +++ b/tests/test_edgecases.py @@ -1,6 +1,5 @@ -from __future__ import annotations - import os +from typing import Optional from pydantic import BaseModel @@ -34,7 +33,7 @@ async def start(request: Request) -> Template: middleware=[], ) client = EsmeraldTestClient(app) - response = client.get("/") + response = client.get("/", follow_redirects=False) assert response.status_code == 301 @@ -55,13 +54,13 @@ async def start(request: Request) -> Template: ), ) client = EsmeraldTestClient(app) - response = client.get("/") + response = client.get("/", follow_redirects=False) assert response.status_code == 301 def test_get_and_post(): @route(methods=["GET", "POST"]) - async def start(request: Request, data: Model | None = Form()) -> bytes: + async def start(request: Request, data: Optional[Model] = Form()) -> bytes: return b"hello world" app = Esmerald( From 5748808e91e8d38883cd8364a30838b045beeb37 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 17 Jan 2025 13:29:46 +0100 Subject: [PATCH 6/7] Changes: - split edge cases tests --- docs/en/docs/release-notes.md | 1 + tests/routing/test_routing_data.py | 26 +++++++++++++++++++ ..._edgecases.py => test_direct_responses.py} | 25 ++---------------- 3 files changed, 29 insertions(+), 23 deletions(-) create mode 100644 tests/routing/test_routing_data.py rename tests/{test_edgecases.py => test_direct_responses.py} (72%) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 88ca4a00..ab0b3530 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -22,6 +22,7 @@ accessible via `from esmerald import Controller`. - Fix escaped " in TemplateResponse. - Fix TemplateResponse's auto-detection of the media-type when used directly. - Don't mangle strings by default for other media-types than json. +- Don't mangle returned responses. ## 3.6.2 diff --git a/tests/routing/test_routing_data.py b/tests/routing/test_routing_data.py new file mode 100644 index 00000000..b18fcef0 --- /dev/null +++ b/tests/routing/test_routing_data.py @@ -0,0 +1,26 @@ +from typing import Optional + +from pydantic import BaseModel + +from esmerald import Esmerald, Form, Request +from esmerald.routing.gateways import Gateway +from esmerald.routing.handlers import route +from esmerald.testclient import EsmeraldTestClient + + +class Model(BaseModel): + id: str + + +def test_get_and_post(): + @route(methods=["GET", "POST"]) + async def start(request: Request, data: Optional[Model] = Form()) -> bytes: + return b"hello world" + + app = Esmerald( + debug=True, + routes=[Gateway("/", handler=start)], + ) + client = EsmeraldTestClient(app) + response = client.get("/") + assert response.status_code == 200 diff --git a/tests/test_edgecases.py b/tests/test_direct_responses.py similarity index 72% rename from tests/test_edgecases.py rename to tests/test_direct_responses.py index bf5ab8c4..5e2ca25e 100644 --- a/tests/test_edgecases.py +++ b/tests/test_direct_responses.py @@ -1,20 +1,13 @@ import os -from typing import Optional -from pydantic import BaseModel - -from esmerald import Esmerald, Form, Redirect, Request, Template +from esmerald import Esmerald, Redirect, Request, Template from esmerald.config.template import TemplateConfig from esmerald.responses.base import RedirectResponse from esmerald.routing.gateways import Gateway -from esmerald.routing.handlers import get, route +from esmerald.routing.handlers import get from esmerald.testclient import EsmeraldTestClient -class Model(BaseModel): - id: str - - def test_return_response_container(template_dir): path = os.path.join(template_dir, "start.html") with open(path, "w") as file: @@ -56,17 +49,3 @@ async def start(request: Request) -> Template: client = EsmeraldTestClient(app) response = client.get("/", follow_redirects=False) assert response.status_code == 301 - - -def test_get_and_post(): - @route(methods=["GET", "POST"]) - async def start(request: Request, data: Optional[Model] = Form()) -> bytes: - return b"hello world" - - app = Esmerald( - debug=True, - routes=[Gateway("/", handler=start)], - ) - client = EsmeraldTestClient(app) - response = client.get("/") - assert response.status_code == 200 From ff97e429e5b4b357ced4e39095f55b6b8142254f Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 17 Jan 2025 15:21:19 +0100 Subject: [PATCH 7/7] fix CI, fix direct responses, handlers are required for cookies --- esmerald/routing/base.py | 32 ++++++++++++++++++++------------ esmerald/routing/router.py | 2 -- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/esmerald/routing/base.py b/esmerald/routing/base.py index 8826a8d3..70ba00b1 100644 --- a/esmerald/routing/base.py +++ b/esmerald/routing/base.py @@ -275,7 +275,9 @@ def _get_response_container_handler( cookies: ResponseCookies, headers: Dict[str, Any], media_type: str, - ) -> Callable[[ResponseContainer, Type[Esmerald], Dict[str, Any]], LilyaResponse]: + ) -> Callable[ + [Union[ResponseContainer, LilyaResponse], Type[Esmerald], Dict[str, Any]], LilyaResponse + ]: """ Creates a handler for ResponseContainer types. @@ -290,22 +292,30 @@ def _get_response_container_handler( """ async def response_content( - data: ResponseContainer, app: Type["Esmerald"], **kwargs: Dict[str, Any] + data: Union[ResponseContainer, LilyaResponse], + app: Type["Esmerald"], + **kwargs: Dict[str, Any], ) -> LilyaResponse: _headers = {**self.get_headers(headers), **data.headers} _cookies = self.get_cookies(data.cookies, cookies) - response: Response = data.to_response( - app=app, - headers=_headers, - status_code=data.status_code or self.status_code, - media_type=media_type, - ) + if isinstance(data, LilyaResponse): + response: LilyaResponse = data + else: + response = data.to_response( + app=app, + headers=_headers, + status_code=data.status_code or self.status_code, + media_type=media_type, + ) for cookie in _cookies: response.set_cookie(**cookie) return response return cast( - Callable[[ResponseContainer, Type["Esmerald"], Dict[str, Any]], LilyaResponse], + Callable[ + [Union[ResponseContainer, LilyaResponse], Type["Esmerald"], Dict[str, Any]], + LilyaResponse, + ], response_content, ) @@ -435,7 +445,7 @@ def _get_default_handler( async def response_content(data: Any, **kwargs: Dict[str, Any]) -> LilyaResponse: data = await self.get_response_data(data=data) _cookies = self.get_cookies(cookies) - if isinstance(data, JSONResponse): + if isinstance(data, LilyaResponse): response = data response.status_code = self.status_code response.background = self.background @@ -523,8 +533,6 @@ async def get_response_for_request( parameter_model=parameter_model, request=request, ) - if isinstance(response_data, LilyaResponse): - return response_data response = await self.to_response( app=scope["app"], diff --git a/esmerald/routing/router.py b/esmerald/routing/router.py index b1d8ff2c..fb205991 100644 --- a/esmerald/routing/router.py +++ b/esmerald/routing/router.py @@ -2270,8 +2270,6 @@ def validate_handler(self) -> None: self.validate_reserved_kwargs() async def to_response(self, app: "Esmerald", data: Any) -> LilyaResponse: - if isinstance(data, LilyaResponse): - return data response_handler = self.get_response_for_handler() return await response_handler(app=app, data=data) # type: ignore[call-arg]