Compare commits
	
		
			6 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 9e889fc20b | ||
|   | bae2d4cb57 | ||
|   | 492d6fd19d | ||
|   | 93a0246c03 | ||
|   | dfd1787a49 | ||
|   | 4998fd54c0 | 
| @@ -1,3 +1,15 @@ | ||||
| Version 21.3.2 | ||||
| -------------- | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
|  | ||||
|   * `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_ | ||||
|     Disable response timeout on websocket connections | ||||
|  | ||||
|   * `#2085 <https://github.com/sanic-org/sanic/pull/2085>`_ | ||||
|     Make sure that blueprints with no slash is maintained when applied | ||||
|  | ||||
| Version 21.3.1 | ||||
| -------------- | ||||
|  | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| __version__ = "21.3.1" | ||||
| __version__ = "21.3.2" | ||||
|   | ||||
| @@ -164,10 +164,12 @@ class ASGIApp: | ||||
|         Read and stream the body in chunks from an incoming ASGI message. | ||||
|         """ | ||||
|         message = await self.transport.receive() | ||||
|         body = message.get("body", b"") | ||||
|         if not message.get("more_body", False): | ||||
|             self.request_body = False | ||||
|             if not body: | ||||
|                 return None | ||||
|         return message.get("body", b"") | ||||
|         return body | ||||
|  | ||||
|     async def __aiter__(self): | ||||
|         while self.request_body: | ||||
|   | ||||
| @@ -1,9 +1,13 @@ | ||||
| from collections.abc import MutableSequence | ||||
| from typing import List, Optional, Union | ||||
| from typing import TYPE_CHECKING, List, Optional, Union | ||||
|  | ||||
| import sanic | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from sanic.blueprints import Blueprint | ||||
|  | ||||
|  | ||||
| class BlueprintGroup(MutableSequence): | ||||
|     """ | ||||
|     This class provides a mechanism to implement a Blueprint Group | ||||
| @@ -56,7 +60,12 @@ class BlueprintGroup(MutableSequence): | ||||
|  | ||||
|     __slots__ = ("_blueprints", "_url_prefix", "_version", "_strict_slashes") | ||||
|  | ||||
|     def __init__(self, url_prefix=None, version=None, strict_slashes=None): | ||||
|     def __init__( | ||||
|         self, | ||||
|         url_prefix: Optional[str] = None, | ||||
|         version: Optional[Union[int, str, float]] = None, | ||||
|         strict_slashes: Optional[bool] = None, | ||||
|     ): | ||||
|         """ | ||||
|         Create a new Blueprint Group | ||||
|  | ||||
| @@ -65,13 +74,13 @@ class BlueprintGroup(MutableSequence): | ||||
|             inherited by each of the Blueprint | ||||
|         :param strict_slashes: URL Strict slash behavior indicator | ||||
|         """ | ||||
|         self._blueprints = [] | ||||
|         self._blueprints: List[Blueprint] = [] | ||||
|         self._url_prefix = url_prefix | ||||
|         self._version = version | ||||
|         self._strict_slashes = strict_slashes | ||||
|  | ||||
|     @property | ||||
|     def url_prefix(self) -> str: | ||||
|     def url_prefix(self) -> Optional[Union[int, str, float]]: | ||||
|         """ | ||||
|         Retrieve the URL prefix being used for the Current Blueprint Group | ||||
|  | ||||
|   | ||||
| @@ -70,7 +70,7 @@ class Blueprint(BaseSanic): | ||||
|         name: str, | ||||
|         url_prefix: Optional[str] = None, | ||||
|         host: Optional[str] = None, | ||||
|         version: Optional[int] = None, | ||||
|         version: Optional[Union[int, str, float]] = None, | ||||
|         strict_slashes: Optional[bool] = None, | ||||
|     ): | ||||
|         super().__init__() | ||||
| @@ -85,7 +85,11 @@ class Blueprint(BaseSanic): | ||||
|         self.routes: List[Route] = [] | ||||
|         self.statics: List[RouteHandler] = [] | ||||
|         self.strict_slashes = strict_slashes | ||||
|         self.url_prefix = url_prefix | ||||
|         self.url_prefix = ( | ||||
|             url_prefix[:-1] | ||||
|             if url_prefix and url_prefix.endswith("/") | ||||
|             else url_prefix | ||||
|         ) | ||||
|         self.version = version | ||||
|         self.websocket_routes: List[Route] = [] | ||||
|  | ||||
|   | ||||
| @@ -82,6 +82,7 @@ class Http: | ||||
|         "request_max_size", | ||||
|         "response", | ||||
|         "response_func", | ||||
|         "response_size", | ||||
|         "response_bytes_left", | ||||
|         "upgrade_websocket", | ||||
|     ] | ||||
| @@ -270,6 +271,7 @@ class Http: | ||||
|         size = len(data) | ||||
|         headers = res.headers | ||||
|         status = res.status | ||||
|         self.response_size = size | ||||
|  | ||||
|         if not isinstance(status, int) or status < 200: | ||||
|             raise RuntimeError(f"Invalid response status {status!r}") | ||||
| @@ -424,7 +426,9 @@ class Http: | ||||
|         req, res = self.request, self.response | ||||
|         extra = { | ||||
|             "status": getattr(res, "status", 0), | ||||
|             "byte": getattr(self, "response_bytes_left", -1), | ||||
|             "byte": getattr( | ||||
|                 self, "response_bytes_left", getattr(self, "response_size", -1) | ||||
|             ), | ||||
|             "host": "UNKNOWN", | ||||
|             "request": "nil", | ||||
|         } | ||||
|   | ||||
| @@ -45,7 +45,7 @@ class RouteMixin: | ||||
|         host: Optional[str] = None, | ||||
|         strict_slashes: Optional[bool] = None, | ||||
|         stream: bool = False, | ||||
|         version: Optional[int] = None, | ||||
|         version: Optional[Union[int, str, float]] = None, | ||||
|         name: Optional[str] = None, | ||||
|         ignore_body: bool = False, | ||||
|         apply: bool = True, | ||||
| @@ -71,7 +71,7 @@ class RouteMixin: | ||||
|  | ||||
|         # Fix case where the user did not prefix the URL with a / | ||||
|         # and will probably get confused as to why it's not working | ||||
|         if not uri.startswith("/"): | ||||
|         if not uri.startswith("/") and (uri or hasattr(self, "router")): | ||||
|             uri = "/" + uri | ||||
|  | ||||
|         if strict_slashes is None: | ||||
|   | ||||
| @@ -33,7 +33,7 @@ class Router(BaseRouter): | ||||
|             return self.resolve( | ||||
|                 path=path, | ||||
|                 method=method, | ||||
|                 extra={"host": host}, | ||||
|                 extra={"host": host} if host else None, | ||||
|             ) | ||||
|         except RoutingNotFound as e: | ||||
|             raise NotFound("Requested URL {} not found".format(e.path)) | ||||
| @@ -161,7 +161,7 @@ class Router(BaseRouter): | ||||
|  | ||||
|     @property | ||||
|     def routes_all(self): | ||||
|         return self.routes | ||||
|         return {route.parts: route for route in self.routes} | ||||
|  | ||||
|     @property | ||||
|     def routes_static(self): | ||||
|   | ||||
| @@ -234,11 +234,16 @@ class HttpProtocol(asyncio.Protocol): | ||||
|             if stage is Stage.IDLE and duration > self.keep_alive_timeout: | ||||
|                 logger.debug("KeepAlive Timeout. Closing connection.") | ||||
|             elif stage is Stage.REQUEST and duration > self.request_timeout: | ||||
|                 logger.debug("Request Timeout. Closing connection.") | ||||
|                 self._http.exception = RequestTimeout("Request Timeout") | ||||
|             elif stage is Stage.HANDLER and self._http.upgrade_websocket: | ||||
|                 logger.debug("Handling websocket. Timeouts disabled.") | ||||
|                 return | ||||
|             elif ( | ||||
|                 stage in (Stage.HANDLER, Stage.RESPONSE, Stage.FAILED) | ||||
|                 and duration > self.response_timeout | ||||
|             ): | ||||
|                 logger.debug("Response Timeout. Closing connection.") | ||||
|                 self._http.exception = ServiceUnavailable("Response Timeout") | ||||
|             else: | ||||
|                 interval = ( | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import asyncio | ||||
| from inspect import isawaitable | ||||
| from typing import Any, Dict, List, Optional, Tuple, Union | ||||
|  | ||||
| from sanic_routing import BaseRouter, Route  # type: ignore | ||||
| from sanic_routing import BaseRouter, Route, RouteGroup  # type: ignore | ||||
| from sanic_routing.exceptions import NotFound  # type: ignore | ||||
| from sanic_routing.utils import path_to_parts  # type: ignore | ||||
|  | ||||
| @@ -20,17 +20,11 @@ RESERVED_NAMESPACES = ( | ||||
|  | ||||
|  | ||||
| class Signal(Route): | ||||
|     def get_handler(self, raw_path, method, _): | ||||
|         method = method or self.router.DEFAULT_METHOD | ||||
|         raw_path = raw_path.lstrip(self.router.delimiter) | ||||
|         try: | ||||
|             return self.handlers[raw_path][method] | ||||
|         except (IndexError, KeyError): | ||||
|             raise self.router.method_handler_exception( | ||||
|                 f"Method '{method}' not found on {self}", | ||||
|                 method=method, | ||||
|                 allowed_methods=set(self.methods[raw_path]), | ||||
|             ) | ||||
|     ... | ||||
|  | ||||
|  | ||||
| class SignalGroup(RouteGroup): | ||||
|     ... | ||||
|  | ||||
|  | ||||
| class SignalRouter(BaseRouter): | ||||
| @@ -38,6 +32,7 @@ class SignalRouter(BaseRouter): | ||||
|         super().__init__( | ||||
|             delimiter=".", | ||||
|             route_class=Signal, | ||||
|             group_class=SignalGroup, | ||||
|             stacking=True, | ||||
|         ) | ||||
|         self.ctx.loop = None | ||||
| @@ -49,7 +44,13 @@ class SignalRouter(BaseRouter): | ||||
|     ): | ||||
|         extra = condition or {} | ||||
|         try: | ||||
|             return self.resolve(f".{event}", extra=extra) | ||||
|             group, param_basket = self.find_route( | ||||
|                 f".{event}", | ||||
|                 self.DEFAULT_METHOD, | ||||
|                 self, | ||||
|                 {"__params__": {}}, | ||||
|                 extra=extra, | ||||
|             ) | ||||
|         except NotFound: | ||||
|             message = "Could not find signal %s" | ||||
|             terms: List[Union[str, Optional[Dict[str, str]]]] = [event] | ||||
| @@ -58,15 +59,19 @@ class SignalRouter(BaseRouter): | ||||
|                 terms.append(extra) | ||||
|             raise NotFound(message % tuple(terms)) | ||||
|  | ||||
|         params = param_basket.pop("__params__") | ||||
|         return group, [route.handler for route in group], params | ||||
|  | ||||
|     async def _dispatch( | ||||
|         self, | ||||
|         event: str, | ||||
|         context: Optional[Dict[str, Any]] = None, | ||||
|         condition: Optional[Dict[str, str]] = None, | ||||
|     ) -> None: | ||||
|         signal, handlers, params = self.get(event, condition=condition) | ||||
|         group, handlers, params = self.get(event, condition=condition) | ||||
|  | ||||
|         signal_event = signal.ctx.event | ||||
|         events = [signal.ctx.event for signal in group] | ||||
|         for signal_event in events: | ||||
|             signal_event.set() | ||||
|         if context: | ||||
|             params.update(context) | ||||
| @@ -78,6 +83,7 @@ class SignalRouter(BaseRouter): | ||||
|                     if isawaitable(maybe_coroutine): | ||||
|                         await maybe_coroutine | ||||
|         finally: | ||||
|             for signal_event in events: | ||||
|                 signal_event.clear() | ||||
|  | ||||
|     async def dispatch( | ||||
| @@ -116,7 +122,7 @@ class SignalRouter(BaseRouter): | ||||
|             handler, | ||||
|             requirements=condition, | ||||
|             name=name, | ||||
|             overwrite=True, | ||||
|             append=True, | ||||
|         )  # type: ignore | ||||
|  | ||||
|     def finalize(self, do_compile: bool = True): | ||||
| @@ -125,7 +131,7 @@ class SignalRouter(BaseRouter): | ||||
|         except RuntimeError: | ||||
|             raise RuntimeError("Cannot finalize signals outside of event loop") | ||||
|  | ||||
|         for signal in self.routes.values(): | ||||
|         for signal in self.routes: | ||||
|             signal.ctx.event = asyncio.Event() | ||||
|  | ||||
|         return super().finalize(do_compile=do_compile) | ||||
|   | ||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
									
									
									
									
								
							| @@ -83,7 +83,7 @@ ujson = "ujson>=1.35" + env_dependency | ||||
| uvloop = "uvloop>=0.5.3" + env_dependency | ||||
|  | ||||
| requirements = [ | ||||
|     "sanic-routing", | ||||
|     "sanic-routing>=0.6.0", | ||||
|     "httptools>=0.0.10", | ||||
|     uvloop, | ||||
|     ujson, | ||||
|   | ||||
| @@ -80,6 +80,12 @@ def test_dont_load_env(): | ||||
|     del environ["SANIC_TEST_ANSWER"] | ||||
|  | ||||
|  | ||||
| # @pytest.mark.parametrize("load_env", [None, False, "", "MYAPP_"]) | ||||
| # def test_load_env_deprecation(load_env): | ||||
| #     with pytest.warns(DeprecationWarning, match=r"21\.12"): | ||||
| #         _ = Sanic(name=__name__, load_env=load_env) | ||||
|  | ||||
|  | ||||
| def test_load_env_prefix(): | ||||
|     environ["MYAPP_TEST_ANSWER"] = "42" | ||||
|     app = Sanic(name=__name__, load_env="MYAPP_") | ||||
| @@ -87,6 +93,14 @@ def test_load_env_prefix(): | ||||
|     del environ["MYAPP_TEST_ANSWER"] | ||||
|  | ||||
|  | ||||
| # @pytest.mark.parametrize("env_prefix", [None, ""]) | ||||
| # def test_empty_load_env_prefix(env_prefix): | ||||
| #     environ["SANIC_TEST_ANSWER"] = "42" | ||||
| #     app = Sanic(name=__name__, env_prefix=env_prefix) | ||||
| #     assert getattr(app.config, "TEST_ANSWER", None) is None | ||||
| #     del environ["SANIC_TEST_ANSWER"] | ||||
|  | ||||
|  | ||||
| def test_load_env_prefix_float_values(): | ||||
|     environ["MYAPP_TEST_ROI"] = "2.3" | ||||
|     app = Sanic(name=__name__, load_env="MYAPP_") | ||||
|   | ||||
| @@ -209,13 +209,13 @@ def test_named_static_routes(): | ||||
|         return text("OK2") | ||||
|  | ||||
|     assert app.router.routes_all[("test",)].name == "app.route_test" | ||||
|     assert app.router.routes_static[("test",)].name == "app.route_test" | ||||
|     assert app.router.routes_static[("test",)][0].name == "app.route_test" | ||||
|     assert app.url_for("route_test") == "/test" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler1") | ||||
|  | ||||
|     assert app.router.routes_all[("pizazz",)].name == "app.route_pizazz" | ||||
|     assert app.router.routes_static[("pizazz",)].name == "app.route_pizazz" | ||||
|     assert app.router.routes_static[("pizazz",)][0].name == "app.route_pizazz" | ||||
|     assert app.url_for("route_pizazz") == "/pizazz" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler2") | ||||
| @@ -347,13 +347,13 @@ def test_static_add_named_route(): | ||||
|     app.add_route(handler2, "/test2", name="route_test2") | ||||
|  | ||||
|     assert app.router.routes_all[("test",)].name == "app.route_test" | ||||
|     assert app.router.routes_static[("test",)].name == "app.route_test" | ||||
|     assert app.router.routes_static[("test",)][0].name == "app.route_test" | ||||
|     assert app.url_for("route_test") == "/test" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler1") | ||||
|  | ||||
|     assert app.router.routes_all[("test2",)].name == "app.route_test2" | ||||
|     assert app.router.routes_static[("test2",)].name == "app.route_test2" | ||||
|     assert app.router.routes_static[("test2",)][0].name == "app.route_test2" | ||||
|     assert app.url_for("route_test2") == "/test2" | ||||
|     with pytest.raises(URLBuildError): | ||||
|         app.url_for("handler2") | ||||
|   | ||||
| @@ -104,7 +104,7 @@ def test_route_assigned_to_request(app): | ||||
|         return response.empty() | ||||
|  | ||||
|     request, _ = app.test_client.get("/") | ||||
|     assert request.route is list(app.router.routes.values())[0] | ||||
|     assert request.route is list(app.router.routes)[0] | ||||
|  | ||||
|  | ||||
| def test_protocol_attribute(app): | ||||
|   | ||||
| @@ -253,6 +253,31 @@ async def test_empty_json_asgi(app): | ||||
|     assert response.body == b"null" | ||||
|  | ||||
|  | ||||
| def test_echo_json(app): | ||||
|     @app.post("/") | ||||
|     async def handler(request): | ||||
|         return json(request.json) | ||||
|  | ||||
|     data = {"foo": "bar"} | ||||
|     request, response = app.test_client.post("/", json=data) | ||||
|  | ||||
|     assert response.status == 200 | ||||
|     assert response.json == data | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_echo_json_asgi(app): | ||||
|     @app.post("/") | ||||
|     async def handler(request): | ||||
|         return json(request.json) | ||||
|  | ||||
|     data = {"foo": "bar"} | ||||
|     request, response = await app.asgi_client.post("/", json=data) | ||||
|  | ||||
|     assert response.status == 200 | ||||
|     assert response.json == data | ||||
|  | ||||
|  | ||||
| def test_invalid_json(app): | ||||
|     @app.post("/") | ||||
|     async def handler(request): | ||||
| @@ -292,6 +317,17 @@ def test_query_string(app): | ||||
|     assert request.args.get("test3", default="My value") == "My value" | ||||
|  | ||||
|  | ||||
| # def test_popped_stays_popped(app): | ||||
| #     @app.route("/") | ||||
| #     async def handler(request): | ||||
| #         return text("OK") | ||||
|  | ||||
| #     request, response = app.test_client.get("/", params=[("test1", "1")]) | ||||
|  | ||||
| #     assert request.args.pop("test1") == ["1"] | ||||
| #     assert "test1" not in request.args | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_query_string_asgi(app): | ||||
|     @app.route("/") | ||||
| @@ -2159,3 +2195,72 @@ def test_safe_method_with_body(app): | ||||
|     assert request.body == data.encode("utf-8") | ||||
|     assert request.json.get("test") == "OK" | ||||
|     assert response.body == b"OK" | ||||
|  | ||||
|  | ||||
| def test_conflicting_body_methods_overload(app): | ||||
|     @app.put("/") | ||||
|     @app.put("/p/") | ||||
|     @app.put("/p/<foo>") | ||||
|     async def put(request, foo=None): | ||||
|         return json( | ||||
|             {"name": request.route.name, "body": str(request.body), "foo": foo} | ||||
|         ) | ||||
|  | ||||
|     @app.delete("/p/<foo>") | ||||
|     async def delete(request, foo): | ||||
|         return json( | ||||
|             {"name": request.route.name, "body": str(request.body), "foo": foo} | ||||
|         ) | ||||
|  | ||||
|     payload = {"test": "OK"} | ||||
|     data = str(json_dumps(payload).encode()) | ||||
|  | ||||
|     _, response = app.test_client.put("/", json=payload) | ||||
|     assert response.status == 200 | ||||
|     assert response.json == { | ||||
|         "name": "test_conflicting_body_methods_overload.put", | ||||
|         "foo": None, | ||||
|         "body": data, | ||||
|     } | ||||
|     _, response = app.test_client.put("/p", json=payload) | ||||
|     assert response.status == 200 | ||||
|     assert response.json == { | ||||
|         "name": "test_conflicting_body_methods_overload.put", | ||||
|         "foo": None, | ||||
|         "body": data, | ||||
|     } | ||||
|     _, response = app.test_client.put("/p/test", json=payload) | ||||
|     assert response.status == 200 | ||||
|     assert response.json == { | ||||
|         "name": "test_conflicting_body_methods_overload.put", | ||||
|         "foo": "test", | ||||
|         "body": data, | ||||
|     } | ||||
|     _, response = app.test_client.delete("/p/test") | ||||
|     assert response.status == 200 | ||||
|     assert response.json == { | ||||
|         "name": "test_conflicting_body_methods_overload.delete", | ||||
|         "foo": "test", | ||||
|         "body": str("".encode()), | ||||
|     } | ||||
|  | ||||
|  | ||||
| def test_handler_overload(app): | ||||
|     @app.get( | ||||
|         "/long/sub/route/param_a/<param_a:string>/param_b/<param_b:string>" | ||||
|     ) | ||||
|     @app.post("/long/sub/route/") | ||||
|     def handler(request, **kwargs): | ||||
|         return json(kwargs) | ||||
|  | ||||
|     _, response = app.test_client.get( | ||||
|         "/long/sub/route/param_a/foo/param_b/bar" | ||||
|     ) | ||||
|     assert response.status == 200 | ||||
|     assert response.json == { | ||||
|         "param_a": "foo", | ||||
|         "param_b": "bar", | ||||
|     } | ||||
|     _, response = app.test_client.post("/long/sub/route") | ||||
|     assert response.status == 200 | ||||
|     assert response.json == {} | ||||
|   | ||||
| @@ -65,7 +65,9 @@ def test_method_not_allowed(): | ||||
|     } | ||||
|  | ||||
|     request, response = app.test_client.post("/") | ||||
|     assert set(response.headers["Allow"].split(", ")) == {"GET", "HEAD"} | ||||
|     assert set(response.headers["Allow"].split(", ")) == { | ||||
|         "GET", | ||||
|     } | ||||
|  | ||||
|     app.router.reset() | ||||
|  | ||||
| @@ -78,7 +80,6 @@ def test_method_not_allowed(): | ||||
|     assert set(response.headers["Allow"].split(", ")) == { | ||||
|         "GET", | ||||
|         "POST", | ||||
|         "HEAD", | ||||
|     } | ||||
|     assert response.headers["Content-Length"] == "0" | ||||
|  | ||||
| @@ -87,7 +88,6 @@ def test_method_not_allowed(): | ||||
|     assert set(response.headers["Allow"].split(", ")) == { | ||||
|         "GET", | ||||
|         "POST", | ||||
|         "HEAD", | ||||
|     } | ||||
|     assert response.headers["Content-Length"] == "0" | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,11 @@ | ||||
| import asyncio | ||||
| import logging | ||||
|  | ||||
| from time import sleep | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.exceptions import ServiceUnavailable | ||||
| from sanic.log import LOGGING_CONFIG_DEFAULTS | ||||
| from sanic.response import text | ||||
|  | ||||
|  | ||||
| @@ -13,6 +17,8 @@ response_timeout_app.config.RESPONSE_TIMEOUT = 1 | ||||
| response_timeout_default_app.config.RESPONSE_TIMEOUT = 1 | ||||
| response_handler_cancelled_app.config.RESPONSE_TIMEOUT = 1 | ||||
|  | ||||
| response_handler_cancelled_app.ctx.flag = False | ||||
|  | ||||
|  | ||||
| @response_timeout_app.route("/1") | ||||
| async def handler_1(request): | ||||
| @@ -25,32 +31,17 @@ def handler_exception(request, exception): | ||||
|     return text("Response Timeout from error_handler.", 503) | ||||
|  | ||||
|  | ||||
| def test_server_error_response_timeout(): | ||||
|     request, response = response_timeout_app.test_client.get("/1") | ||||
|     assert response.status == 503 | ||||
|     assert response.text == "Response Timeout from error_handler." | ||||
|  | ||||
|  | ||||
| @response_timeout_default_app.route("/1") | ||||
| async def handler_2(request): | ||||
|     await asyncio.sleep(2) | ||||
|     return text("OK") | ||||
|  | ||||
|  | ||||
| def test_default_server_error_response_timeout(): | ||||
|     request, response = response_timeout_default_app.test_client.get("/1") | ||||
|     assert response.status == 503 | ||||
|     assert "Response Timeout" in response.text | ||||
|  | ||||
|  | ||||
| response_handler_cancelled_app.flag = False | ||||
|  | ||||
|  | ||||
| @response_handler_cancelled_app.exception(asyncio.CancelledError) | ||||
| def handler_cancelled(request, exception): | ||||
|     # If we get a CancelledError, it means sanic has already sent a response, | ||||
|     # we should not ever have to handle a CancelledError. | ||||
|     response_handler_cancelled_app.flag = True | ||||
|     response_handler_cancelled_app.ctx.flag = True | ||||
|     return text("App received CancelledError!", 500) | ||||
|     # The client will never receive this response, because the socket | ||||
|     # is already closed when we get a CancelledError. | ||||
| @@ -62,8 +53,44 @@ async def handler_3(request): | ||||
|     return text("OK") | ||||
|  | ||||
|  | ||||
| def test_server_error_response_timeout(): | ||||
|     request, response = response_timeout_app.test_client.get("/1") | ||||
|     assert response.status == 503 | ||||
|     assert response.text == "Response Timeout from error_handler." | ||||
|  | ||||
|  | ||||
| def test_default_server_error_response_timeout(): | ||||
|     request, response = response_timeout_default_app.test_client.get("/1") | ||||
|     assert response.status == 503 | ||||
|     assert "Response Timeout" in response.text | ||||
|  | ||||
|  | ||||
| def test_response_handler_cancelled(): | ||||
|     request, response = response_handler_cancelled_app.test_client.get("/1") | ||||
|     assert response.status == 503 | ||||
|     assert "Response Timeout" in response.text | ||||
|     assert response_handler_cancelled_app.flag is False | ||||
|     assert response_handler_cancelled_app.ctx.flag is False | ||||
|  | ||||
|  | ||||
| def test_response_timeout_not_applied(caplog): | ||||
|     modified_config = LOGGING_CONFIG_DEFAULTS | ||||
|     modified_config["loggers"]["sanic.root"]["level"] = "DEBUG" | ||||
|  | ||||
|     app = Sanic("test_logging", log_config=modified_config) | ||||
|     app.config.RESPONSE_TIMEOUT = 1 | ||||
|     app.ctx.event = asyncio.Event() | ||||
|  | ||||
|     @app.websocket("/ws") | ||||
|     async def ws_handler(request, ws): | ||||
|         sleep(2) | ||||
|         await asyncio.sleep(0) | ||||
|         request.app.ctx.event.set() | ||||
|  | ||||
|     with caplog.at_level(logging.DEBUG): | ||||
|         _ = app.test_client.websocket("/ws") | ||||
|     assert app.ctx.event.is_set() | ||||
|     assert ( | ||||
|         "sanic.root", | ||||
|         10, | ||||
|         "Handling websocket. Timeouts disabled.", | ||||
|     ) in caplog.record_tuples | ||||
|   | ||||
| @@ -543,9 +543,6 @@ def test_dynamic_route_regex(app): | ||||
|     async def handler(request, folder_id): | ||||
|         return text("OK") | ||||
|  | ||||
|     app.router.finalize() | ||||
|     print(app.router.find_route_src) | ||||
|  | ||||
|     request, response = app.test_client.get("/folder/test") | ||||
|     assert response.status == 200 | ||||
|  | ||||
| @@ -587,6 +584,9 @@ def test_dynamic_route_path(app): | ||||
|     async def handler(request, path): | ||||
|         return text("OK") | ||||
|  | ||||
|     app.router.finalize() | ||||
|     print(app.router.find_route_src) | ||||
|  | ||||
|     request, response = app.test_client.get("/path/1/info") | ||||
|     assert response.status == 200 | ||||
|  | ||||
| @@ -1008,14 +1008,8 @@ def test_unmergeable_overload_routes(app): | ||||
|     async def handler2(request): | ||||
|         return text("OK1") | ||||
|  | ||||
|     assert ( | ||||
|         len( | ||||
|             dict(list(app.router.static_routes.values())[0].handlers)[ | ||||
|                 "overload_whole" | ||||
|             ] | ||||
|         ) | ||||
|         == 3 | ||||
|     ) | ||||
|     assert len(app.router.static_routes) == 1 | ||||
|     assert len(app.router.static_routes[("overload_whole",)].methods) == 3 | ||||
|  | ||||
|     request, response = app.test_client.get("/overload_whole") | ||||
|     assert response.text == "OK1" | ||||
| @@ -1175,3 +1169,59 @@ def test_route_with_bad_named_param(app): | ||||
|  | ||||
|     with pytest.raises(SanicException): | ||||
|         app.router.finalize() | ||||
|  | ||||
|  | ||||
| def test_routes_with_and_without_slash_definitions(app): | ||||
|     bar = Blueprint("bar", url_prefix="bar") | ||||
|     baz = Blueprint("baz", url_prefix="/baz") | ||||
|     fizz = Blueprint("fizz", url_prefix="fizz/") | ||||
|     buzz = Blueprint("buzz", url_prefix="/buzz/") | ||||
|  | ||||
|     instances = ( | ||||
|         (app, "foo"), | ||||
|         (bar, "bar"), | ||||
|         (baz, "baz"), | ||||
|         (fizz, "fizz"), | ||||
|         (buzz, "buzz"), | ||||
|     ) | ||||
|  | ||||
|     for instance, term in instances: | ||||
|         route = f"/{term}" if isinstance(instance, Sanic) else "" | ||||
|  | ||||
|         @instance.get(route, strict_slashes=True) | ||||
|         def get_without(request): | ||||
|             return text(f"{term}_without") | ||||
|  | ||||
|         @instance.get(f"{route}/", strict_slashes=True) | ||||
|         def get_with(request): | ||||
|             return text(f"{term}_with") | ||||
|  | ||||
|         @instance.post(route, strict_slashes=True) | ||||
|         def post_without(request): | ||||
|             return text(f"{term}_without") | ||||
|  | ||||
|         @instance.post(f"{route}/", strict_slashes=True) | ||||
|         def post_with(request): | ||||
|             return text(f"{term}_with") | ||||
|  | ||||
|     app.blueprint(bar) | ||||
|     app.blueprint(baz) | ||||
|     app.blueprint(fizz) | ||||
|     app.blueprint(buzz) | ||||
|  | ||||
|     for _, term in instances: | ||||
|         _, response = app.test_client.get(f"/{term}") | ||||
|         assert response.status == 200 | ||||
|         assert response.text == f"{term}_without" | ||||
|  | ||||
|         _, response = app.test_client.get(f"/{term}/") | ||||
|         assert response.status == 200 | ||||
|         assert response.text == f"{term}_with" | ||||
|  | ||||
|         _, response = app.test_client.post(f"/{term}") | ||||
|         assert response.status == 200 | ||||
|         assert response.text == f"{term}_without" | ||||
|  | ||||
|         _, response = app.test_client.post(f"/{term}/") | ||||
|         assert response.status == 200 | ||||
|         assert response.text == f"{term}_with" | ||||
|   | ||||
| @@ -28,7 +28,8 @@ def test_add_signal_decorator(app): | ||||
|     async def async_signal(*_): | ||||
|         ... | ||||
|  | ||||
|     assert len(app.signal_router.routes) == 1 | ||||
|     assert len(app.signal_router.routes) == 2 | ||||
|     assert len(app.signal_router.dynamic_routes) == 1 | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
| @@ -79,13 +80,13 @@ async def test_dispatch_signal_triggers_triggers_event(app): | ||||
|     def sync_signal(*args): | ||||
|         nonlocal app | ||||
|         nonlocal counter | ||||
|         signal, *_ = app.signal_router.get("foo.bar.baz") | ||||
|         group, *_ = app.signal_router.get("foo.bar.baz") | ||||
|         for signal in group: | ||||
|             counter += signal.ctx.event.is_set() | ||||
|  | ||||
|     app.signal_router.finalize() | ||||
|  | ||||
|     await app.dispatch("foo.bar.baz") | ||||
|     signal, *_ = app.signal_router.get("foo.bar.baz") | ||||
|  | ||||
|     assert counter == 1 | ||||
|  | ||||
| @@ -224,7 +225,7 @@ async def test_dispatch_signal_triggers_event_on_bp(app): | ||||
|  | ||||
|     app.blueprint(bp) | ||||
|     app.signal_router.finalize() | ||||
|     signal, *_ = app.signal_router.get( | ||||
|     signal_group, *_ = app.signal_router.get( | ||||
|         "foo.bar.baz", condition={"blueprint": "bp"} | ||||
|     ) | ||||
|  | ||||
| @@ -233,6 +234,7 @@ async def test_dispatch_signal_triggers_event_on_bp(app): | ||||
|     assert isawaitable(waiter) | ||||
|  | ||||
|     fut = asyncio.ensure_future(do_wait()) | ||||
|     for signal in signal_group: | ||||
|         signal.ctx.event.set() | ||||
|     await fut | ||||
|  | ||||
|   | ||||
| @@ -1,11 +1,16 @@ | ||||
| import inspect | ||||
| import logging | ||||
| import os | ||||
|  | ||||
| from collections import Counter | ||||
| from pathlib import Path | ||||
| from time import gmtime, strftime | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic import text | ||||
| from sanic.exceptions import FileNotFound | ||||
|  | ||||
|  | ||||
| @pytest.fixture(scope="module") | ||||
| def static_file_directory(): | ||||
| @@ -454,3 +459,51 @@ def test_nested_dir(app, static_file_directory): | ||||
|  | ||||
|     assert response.status == 200 | ||||
|     assert response.text == "foo\n" | ||||
|  | ||||
|  | ||||
| def test_stack_trace_on_not_found(app, static_file_directory, caplog): | ||||
|     app.static("/static", static_file_directory) | ||||
|  | ||||
|     with caplog.at_level(logging.INFO): | ||||
|         _, response = app.test_client.get("/static/non_existing_file.file") | ||||
|  | ||||
|     counter = Counter([r[1] for r in caplog.record_tuples]) | ||||
|  | ||||
|     assert response.status == 404 | ||||
|     assert counter[logging.INFO] == 5 | ||||
|     assert counter[logging.ERROR] == 1 | ||||
|  | ||||
|  | ||||
| # def test_no_stack_trace_on_not_found(app, static_file_directory, caplog): | ||||
| #     app.static("/static", static_file_directory) | ||||
|  | ||||
| #     @app.exception(FileNotFound) | ||||
| #     async def file_not_found(request, exception): | ||||
| #         return text(f"No file: {request.path}", status=404) | ||||
|  | ||||
| #     with caplog.at_level(logging.INFO): | ||||
| #         _, response = app.test_client.get("/static/non_existing_file.file") | ||||
|  | ||||
| #     counter = Counter([r[1] for r in caplog.record_tuples]) | ||||
|  | ||||
| #     assert response.status == 404 | ||||
| #     assert counter[logging.INFO] == 5 | ||||
| #     assert logging.ERROR not in counter | ||||
| #     assert response.text == "No file: /static/non_existing_file.file" | ||||
|  | ||||
|  | ||||
| def test_multiple_statics(app, static_file_directory): | ||||
|     app.static("/file", get_file_path(static_file_directory, "test.file")) | ||||
|     app.static("/png", get_file_path(static_file_directory, "python.png")) | ||||
|  | ||||
|     _, response = app.test_client.get("/file") | ||||
|     assert response.status == 200 | ||||
|     assert response.body == get_file_content( | ||||
|         static_file_directory, "test.file" | ||||
|     ) | ||||
|  | ||||
|     _, response = app.test_client.get("/png") | ||||
|     assert response.status == 200 | ||||
|     assert response.body == get_file_content( | ||||
|         static_file_directory, "python.png" | ||||
|     ) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user