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 | 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. |         Read and stream the body in chunks from an incoming ASGI message. | ||||||
|         """ |         """ | ||||||
|         message = await self.transport.receive() |         message = await self.transport.receive() | ||||||
|  |         body = message.get("body", b"") | ||||||
|         if not message.get("more_body", False): |         if not message.get("more_body", False): | ||||||
|             self.request_body = False |             self.request_body = False | ||||||
|             return None |             if not body: | ||||||
|         return message.get("body", b"") |                 return None | ||||||
|  |         return body | ||||||
|  |  | ||||||
|     async def __aiter__(self): |     async def __aiter__(self): | ||||||
|         while self.request_body: |         while self.request_body: | ||||||
|   | |||||||
| @@ -1,9 +1,13 @@ | |||||||
| from collections.abc import MutableSequence | from collections.abc import MutableSequence | ||||||
| from typing import List, Optional, Union | from typing import TYPE_CHECKING, List, Optional, Union | ||||||
|  |  | ||||||
| import sanic | import sanic | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from sanic.blueprints import Blueprint | ||||||
|  |  | ||||||
|  |  | ||||||
| class BlueprintGroup(MutableSequence): | class BlueprintGroup(MutableSequence): | ||||||
|     """ |     """ | ||||||
|     This class provides a mechanism to implement a Blueprint Group |     This class provides a mechanism to implement a Blueprint Group | ||||||
| @@ -56,7 +60,12 @@ class BlueprintGroup(MutableSequence): | |||||||
|  |  | ||||||
|     __slots__ = ("_blueprints", "_url_prefix", "_version", "_strict_slashes") |     __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 |         Create a new Blueprint Group | ||||||
|  |  | ||||||
| @@ -65,13 +74,13 @@ class BlueprintGroup(MutableSequence): | |||||||
|             inherited by each of the Blueprint |             inherited by each of the Blueprint | ||||||
|         :param strict_slashes: URL Strict slash behavior indicator |         :param strict_slashes: URL Strict slash behavior indicator | ||||||
|         """ |         """ | ||||||
|         self._blueprints = [] |         self._blueprints: List[Blueprint] = [] | ||||||
|         self._url_prefix = url_prefix |         self._url_prefix = url_prefix | ||||||
|         self._version = version |         self._version = version | ||||||
|         self._strict_slashes = strict_slashes |         self._strict_slashes = strict_slashes | ||||||
|  |  | ||||||
|     @property |     @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 |         Retrieve the URL prefix being used for the Current Blueprint Group | ||||||
|  |  | ||||||
|   | |||||||
| @@ -70,7 +70,7 @@ class Blueprint(BaseSanic): | |||||||
|         name: str, |         name: str, | ||||||
|         url_prefix: Optional[str] = None, |         url_prefix: Optional[str] = None, | ||||||
|         host: Optional[str] = None, |         host: Optional[str] = None, | ||||||
|         version: Optional[int] = None, |         version: Optional[Union[int, str, float]] = None, | ||||||
|         strict_slashes: Optional[bool] = None, |         strict_slashes: Optional[bool] = None, | ||||||
|     ): |     ): | ||||||
|         super().__init__() |         super().__init__() | ||||||
| @@ -85,7 +85,11 @@ class Blueprint(BaseSanic): | |||||||
|         self.routes: List[Route] = [] |         self.routes: List[Route] = [] | ||||||
|         self.statics: List[RouteHandler] = [] |         self.statics: List[RouteHandler] = [] | ||||||
|         self.strict_slashes = strict_slashes |         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.version = version | ||||||
|         self.websocket_routes: List[Route] = [] |         self.websocket_routes: List[Route] = [] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -82,6 +82,7 @@ class Http: | |||||||
|         "request_max_size", |         "request_max_size", | ||||||
|         "response", |         "response", | ||||||
|         "response_func", |         "response_func", | ||||||
|  |         "response_size", | ||||||
|         "response_bytes_left", |         "response_bytes_left", | ||||||
|         "upgrade_websocket", |         "upgrade_websocket", | ||||||
|     ] |     ] | ||||||
| @@ -270,6 +271,7 @@ class Http: | |||||||
|         size = len(data) |         size = len(data) | ||||||
|         headers = res.headers |         headers = res.headers | ||||||
|         status = res.status |         status = res.status | ||||||
|  |         self.response_size = size | ||||||
|  |  | ||||||
|         if not isinstance(status, int) or status < 200: |         if not isinstance(status, int) or status < 200: | ||||||
|             raise RuntimeError(f"Invalid response status {status!r}") |             raise RuntimeError(f"Invalid response status {status!r}") | ||||||
| @@ -424,7 +426,9 @@ class Http: | |||||||
|         req, res = self.request, self.response |         req, res = self.request, self.response | ||||||
|         extra = { |         extra = { | ||||||
|             "status": getattr(res, "status", 0), |             "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", |             "host": "UNKNOWN", | ||||||
|             "request": "nil", |             "request": "nil", | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -45,7 +45,7 @@ class RouteMixin: | |||||||
|         host: Optional[str] = None, |         host: Optional[str] = None, | ||||||
|         strict_slashes: Optional[bool] = None, |         strict_slashes: Optional[bool] = None, | ||||||
|         stream: bool = False, |         stream: bool = False, | ||||||
|         version: Optional[int] = None, |         version: Optional[Union[int, str, float]] = None, | ||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|         ignore_body: bool = False, |         ignore_body: bool = False, | ||||||
|         apply: bool = True, |         apply: bool = True, | ||||||
| @@ -71,7 +71,7 @@ class RouteMixin: | |||||||
|  |  | ||||||
|         # Fix case where the user did not prefix the URL with a / |         # Fix case where the user did not prefix the URL with a / | ||||||
|         # and will probably get confused as to why it's not working |         # 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 |             uri = "/" + uri | ||||||
|  |  | ||||||
|         if strict_slashes is None: |         if strict_slashes is None: | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ class Router(BaseRouter): | |||||||
|             return self.resolve( |             return self.resolve( | ||||||
|                 path=path, |                 path=path, | ||||||
|                 method=method, |                 method=method, | ||||||
|                 extra={"host": host}, |                 extra={"host": host} if host else None, | ||||||
|             ) |             ) | ||||||
|         except RoutingNotFound as e: |         except RoutingNotFound as e: | ||||||
|             raise NotFound("Requested URL {} not found".format(e.path)) |             raise NotFound("Requested URL {} not found".format(e.path)) | ||||||
| @@ -161,7 +161,7 @@ class Router(BaseRouter): | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def routes_all(self): |     def routes_all(self): | ||||||
|         return self.routes |         return {route.parts: route for route in self.routes} | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def routes_static(self): |     def routes_static(self): | ||||||
|   | |||||||
| @@ -234,11 +234,16 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             if stage is Stage.IDLE and duration > self.keep_alive_timeout: |             if stage is Stage.IDLE and duration > self.keep_alive_timeout: | ||||||
|                 logger.debug("KeepAlive Timeout. Closing connection.") |                 logger.debug("KeepAlive Timeout. Closing connection.") | ||||||
|             elif stage is Stage.REQUEST and duration > self.request_timeout: |             elif stage is Stage.REQUEST and duration > self.request_timeout: | ||||||
|  |                 logger.debug("Request Timeout. Closing connection.") | ||||||
|                 self._http.exception = RequestTimeout("Request Timeout") |                 self._http.exception = RequestTimeout("Request Timeout") | ||||||
|  |             elif stage is Stage.HANDLER and self._http.upgrade_websocket: | ||||||
|  |                 logger.debug("Handling websocket. Timeouts disabled.") | ||||||
|  |                 return | ||||||
|             elif ( |             elif ( | ||||||
|                 stage in (Stage.HANDLER, Stage.RESPONSE, Stage.FAILED) |                 stage in (Stage.HANDLER, Stage.RESPONSE, Stage.FAILED) | ||||||
|                 and duration > self.response_timeout |                 and duration > self.response_timeout | ||||||
|             ): |             ): | ||||||
|  |                 logger.debug("Response Timeout. Closing connection.") | ||||||
|                 self._http.exception = ServiceUnavailable("Response Timeout") |                 self._http.exception = ServiceUnavailable("Response Timeout") | ||||||
|             else: |             else: | ||||||
|                 interval = ( |                 interval = ( | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import asyncio | |||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
| from typing import Any, Dict, List, Optional, Tuple, Union | 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.exceptions import NotFound  # type: ignore | ||||||
| from sanic_routing.utils import path_to_parts  # type: ignore | from sanic_routing.utils import path_to_parts  # type: ignore | ||||||
|  |  | ||||||
| @@ -20,17 +20,11 @@ RESERVED_NAMESPACES = ( | |||||||
|  |  | ||||||
|  |  | ||||||
| class Signal(Route): | 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: | class SignalGroup(RouteGroup): | ||||||
|             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 SignalRouter(BaseRouter): | class SignalRouter(BaseRouter): | ||||||
| @@ -38,6 +32,7 @@ class SignalRouter(BaseRouter): | |||||||
|         super().__init__( |         super().__init__( | ||||||
|             delimiter=".", |             delimiter=".", | ||||||
|             route_class=Signal, |             route_class=Signal, | ||||||
|  |             group_class=SignalGroup, | ||||||
|             stacking=True, |             stacking=True, | ||||||
|         ) |         ) | ||||||
|         self.ctx.loop = None |         self.ctx.loop = None | ||||||
| @@ -49,7 +44,13 @@ class SignalRouter(BaseRouter): | |||||||
|     ): |     ): | ||||||
|         extra = condition or {} |         extra = condition or {} | ||||||
|         try: |         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: |         except NotFound: | ||||||
|             message = "Could not find signal %s" |             message = "Could not find signal %s" | ||||||
|             terms: List[Union[str, Optional[Dict[str, str]]]] = [event] |             terms: List[Union[str, Optional[Dict[str, str]]]] = [event] | ||||||
| @@ -58,16 +59,20 @@ class SignalRouter(BaseRouter): | |||||||
|                 terms.append(extra) |                 terms.append(extra) | ||||||
|             raise NotFound(message % tuple(terms)) |             raise NotFound(message % tuple(terms)) | ||||||
|  |  | ||||||
|  |         params = param_basket.pop("__params__") | ||||||
|  |         return group, [route.handler for route in group], params | ||||||
|  |  | ||||||
|     async def _dispatch( |     async def _dispatch( | ||||||
|         self, |         self, | ||||||
|         event: str, |         event: str, | ||||||
|         context: Optional[Dict[str, Any]] = None, |         context: Optional[Dict[str, Any]] = None, | ||||||
|         condition: Optional[Dict[str, str]] = None, |         condition: Optional[Dict[str, str]] = None, | ||||||
|     ) -> 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] | ||||||
|         signal_event.set() |         for signal_event in events: | ||||||
|  |             signal_event.set() | ||||||
|         if context: |         if context: | ||||||
|             params.update(context) |             params.update(context) | ||||||
|  |  | ||||||
| @@ -78,7 +83,8 @@ class SignalRouter(BaseRouter): | |||||||
|                     if isawaitable(maybe_coroutine): |                     if isawaitable(maybe_coroutine): | ||||||
|                         await maybe_coroutine |                         await maybe_coroutine | ||||||
|         finally: |         finally: | ||||||
|             signal_event.clear() |             for signal_event in events: | ||||||
|  |                 signal_event.clear() | ||||||
|  |  | ||||||
|     async def dispatch( |     async def dispatch( | ||||||
|         self, |         self, | ||||||
| @@ -116,7 +122,7 @@ class SignalRouter(BaseRouter): | |||||||
|             handler, |             handler, | ||||||
|             requirements=condition, |             requirements=condition, | ||||||
|             name=name, |             name=name, | ||||||
|             overwrite=True, |             append=True, | ||||||
|         )  # type: ignore |         )  # type: ignore | ||||||
|  |  | ||||||
|     def finalize(self, do_compile: bool = True): |     def finalize(self, do_compile: bool = True): | ||||||
| @@ -125,7 +131,7 @@ class SignalRouter(BaseRouter): | |||||||
|         except RuntimeError: |         except RuntimeError: | ||||||
|             raise RuntimeError("Cannot finalize signals outside of event loop") |             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() |             signal.ctx.event = asyncio.Event() | ||||||
|  |  | ||||||
|         return super().finalize(do_compile=do_compile) |         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 | uvloop = "uvloop>=0.5.3" + env_dependency | ||||||
|  |  | ||||||
| requirements = [ | requirements = [ | ||||||
|     "sanic-routing", |     "sanic-routing>=0.6.0", | ||||||
|     "httptools>=0.0.10", |     "httptools>=0.0.10", | ||||||
|     uvloop, |     uvloop, | ||||||
|     ujson, |     ujson, | ||||||
|   | |||||||
| @@ -80,6 +80,12 @@ def test_dont_load_env(): | |||||||
|     del environ["SANIC_TEST_ANSWER"] |     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(): | def test_load_env_prefix(): | ||||||
|     environ["MYAPP_TEST_ANSWER"] = "42" |     environ["MYAPP_TEST_ANSWER"] = "42" | ||||||
|     app = Sanic(name=__name__, load_env="MYAPP_") |     app = Sanic(name=__name__, load_env="MYAPP_") | ||||||
| @@ -87,6 +93,14 @@ def test_load_env_prefix(): | |||||||
|     del environ["MYAPP_TEST_ANSWER"] |     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(): | def test_load_env_prefix_float_values(): | ||||||
|     environ["MYAPP_TEST_ROI"] = "2.3" |     environ["MYAPP_TEST_ROI"] = "2.3" | ||||||
|     app = Sanic(name=__name__, load_env="MYAPP_") |     app = Sanic(name=__name__, load_env="MYAPP_") | ||||||
|   | |||||||
| @@ -209,13 +209,13 @@ def test_named_static_routes(): | |||||||
|         return text("OK2") |         return text("OK2") | ||||||
|  |  | ||||||
|     assert app.router.routes_all[("test",)].name == "app.route_test" |     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" |     assert app.url_for("route_test") == "/test" | ||||||
|     with pytest.raises(URLBuildError): |     with pytest.raises(URLBuildError): | ||||||
|         app.url_for("handler1") |         app.url_for("handler1") | ||||||
|  |  | ||||||
|     assert app.router.routes_all[("pizazz",)].name == "app.route_pizazz" |     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" |     assert app.url_for("route_pizazz") == "/pizazz" | ||||||
|     with pytest.raises(URLBuildError): |     with pytest.raises(URLBuildError): | ||||||
|         app.url_for("handler2") |         app.url_for("handler2") | ||||||
| @@ -347,13 +347,13 @@ def test_static_add_named_route(): | |||||||
|     app.add_route(handler2, "/test2", name="route_test2") |     app.add_route(handler2, "/test2", name="route_test2") | ||||||
|  |  | ||||||
|     assert app.router.routes_all[("test",)].name == "app.route_test" |     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" |     assert app.url_for("route_test") == "/test" | ||||||
|     with pytest.raises(URLBuildError): |     with pytest.raises(URLBuildError): | ||||||
|         app.url_for("handler1") |         app.url_for("handler1") | ||||||
|  |  | ||||||
|     assert app.router.routes_all[("test2",)].name == "app.route_test2" |     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" |     assert app.url_for("route_test2") == "/test2" | ||||||
|     with pytest.raises(URLBuildError): |     with pytest.raises(URLBuildError): | ||||||
|         app.url_for("handler2") |         app.url_for("handler2") | ||||||
|   | |||||||
| @@ -104,7 +104,7 @@ def test_route_assigned_to_request(app): | |||||||
|         return response.empty() |         return response.empty() | ||||||
|  |  | ||||||
|     request, _ = app.test_client.get("/") |     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): | def test_protocol_attribute(app): | ||||||
|   | |||||||
| @@ -253,6 +253,31 @@ async def test_empty_json_asgi(app): | |||||||
|     assert response.body == b"null" |     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): | def test_invalid_json(app): | ||||||
|     @app.post("/") |     @app.post("/") | ||||||
|     async def handler(request): |     async def handler(request): | ||||||
| @@ -292,6 +317,17 @@ def test_query_string(app): | |||||||
|     assert request.args.get("test3", default="My value") == "My value" |     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 | @pytest.mark.asyncio | ||||||
| async def test_query_string_asgi(app): | async def test_query_string_asgi(app): | ||||||
|     @app.route("/") |     @app.route("/") | ||||||
| @@ -2159,3 +2195,72 @@ def test_safe_method_with_body(app): | |||||||
|     assert request.body == data.encode("utf-8") |     assert request.body == data.encode("utf-8") | ||||||
|     assert request.json.get("test") == "OK" |     assert request.json.get("test") == "OK" | ||||||
|     assert response.body == b"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("/") |     request, response = app.test_client.post("/") | ||||||
|     assert set(response.headers["Allow"].split(", ")) == {"GET", "HEAD"} |     assert set(response.headers["Allow"].split(", ")) == { | ||||||
|  |         "GET", | ||||||
|  |     } | ||||||
|  |  | ||||||
|     app.router.reset() |     app.router.reset() | ||||||
|  |  | ||||||
| @@ -78,7 +80,6 @@ def test_method_not_allowed(): | |||||||
|     assert set(response.headers["Allow"].split(", ")) == { |     assert set(response.headers["Allow"].split(", ")) == { | ||||||
|         "GET", |         "GET", | ||||||
|         "POST", |         "POST", | ||||||
|         "HEAD", |  | ||||||
|     } |     } | ||||||
|     assert response.headers["Content-Length"] == "0" |     assert response.headers["Content-Length"] == "0" | ||||||
|  |  | ||||||
| @@ -87,7 +88,6 @@ def test_method_not_allowed(): | |||||||
|     assert set(response.headers["Allow"].split(", ")) == { |     assert set(response.headers["Allow"].split(", ")) == { | ||||||
|         "GET", |         "GET", | ||||||
|         "POST", |         "POST", | ||||||
|         "HEAD", |  | ||||||
|     } |     } | ||||||
|     assert response.headers["Content-Length"] == "0" |     assert response.headers["Content-Length"] == "0" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,11 @@ | |||||||
| import asyncio | import asyncio | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  | from time import sleep | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.exceptions import ServiceUnavailable | from sanic.exceptions import ServiceUnavailable | ||||||
|  | from sanic.log import LOGGING_CONFIG_DEFAULTS | ||||||
| from sanic.response import text | 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_timeout_default_app.config.RESPONSE_TIMEOUT = 1 | ||||||
| response_handler_cancelled_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") | @response_timeout_app.route("/1") | ||||||
| async def handler_1(request): | async def handler_1(request): | ||||||
| @@ -25,32 +31,17 @@ def handler_exception(request, exception): | |||||||
|     return text("Response Timeout from error_handler.", 503) |     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") | @response_timeout_default_app.route("/1") | ||||||
| async def handler_2(request): | async def handler_2(request): | ||||||
|     await asyncio.sleep(2) |     await asyncio.sleep(2) | ||||||
|     return text("OK") |     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) | @response_handler_cancelled_app.exception(asyncio.CancelledError) | ||||||
| def handler_cancelled(request, exception): | def handler_cancelled(request, exception): | ||||||
|     # If we get a CancelledError, it means sanic has already sent a response, |     # If we get a CancelledError, it means sanic has already sent a response, | ||||||
|     # we should not ever have to handle a CancelledError. |     # 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) |     return text("App received CancelledError!", 500) | ||||||
|     # The client will never receive this response, because the socket |     # The client will never receive this response, because the socket | ||||||
|     # is already closed when we get a CancelledError. |     # is already closed when we get a CancelledError. | ||||||
| @@ -62,8 +53,44 @@ async def handler_3(request): | |||||||
|     return text("OK") |     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(): | def test_response_handler_cancelled(): | ||||||
|     request, response = response_handler_cancelled_app.test_client.get("/1") |     request, response = response_handler_cancelled_app.test_client.get("/1") | ||||||
|     assert response.status == 503 |     assert response.status == 503 | ||||||
|     assert "Response Timeout" in response.text |     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): |     async def handler(request, folder_id): | ||||||
|         return text("OK") |         return text("OK") | ||||||
|  |  | ||||||
|     app.router.finalize() |  | ||||||
|     print(app.router.find_route_src) |  | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/folder/test") |     request, response = app.test_client.get("/folder/test") | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|  |  | ||||||
| @@ -587,6 +584,9 @@ def test_dynamic_route_path(app): | |||||||
|     async def handler(request, path): |     async def handler(request, path): | ||||||
|         return text("OK") |         return text("OK") | ||||||
|  |  | ||||||
|  |     app.router.finalize() | ||||||
|  |     print(app.router.find_route_src) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/path/1/info") |     request, response = app.test_client.get("/path/1/info") | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|  |  | ||||||
| @@ -1008,14 +1008,8 @@ def test_unmergeable_overload_routes(app): | |||||||
|     async def handler2(request): |     async def handler2(request): | ||||||
|         return text("OK1") |         return text("OK1") | ||||||
|  |  | ||||||
|     assert ( |     assert len(app.router.static_routes) == 1 | ||||||
|         len( |     assert len(app.router.static_routes[("overload_whole",)].methods) == 3 | ||||||
|             dict(list(app.router.static_routes.values())[0].handlers)[ |  | ||||||
|                 "overload_whole" |  | ||||||
|             ] |  | ||||||
|         ) |  | ||||||
|         == 3 |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     request, response = app.test_client.get("/overload_whole") |     request, response = app.test_client.get("/overload_whole") | ||||||
|     assert response.text == "OK1" |     assert response.text == "OK1" | ||||||
| @@ -1175,3 +1169,59 @@ def test_route_with_bad_named_param(app): | |||||||
|  |  | ||||||
|     with pytest.raises(SanicException): |     with pytest.raises(SanicException): | ||||||
|         app.router.finalize() |         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(*_): |     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( | @pytest.mark.parametrize( | ||||||
| @@ -79,13 +80,13 @@ async def test_dispatch_signal_triggers_triggers_event(app): | |||||||
|     def sync_signal(*args): |     def sync_signal(*args): | ||||||
|         nonlocal app |         nonlocal app | ||||||
|         nonlocal counter |         nonlocal counter | ||||||
|         signal, *_ = app.signal_router.get("foo.bar.baz") |         group, *_ = app.signal_router.get("foo.bar.baz") | ||||||
|         counter += signal.ctx.event.is_set() |         for signal in group: | ||||||
|  |             counter += signal.ctx.event.is_set() | ||||||
|  |  | ||||||
|     app.signal_router.finalize() |     app.signal_router.finalize() | ||||||
|  |  | ||||||
|     await app.dispatch("foo.bar.baz") |     await app.dispatch("foo.bar.baz") | ||||||
|     signal, *_ = app.signal_router.get("foo.bar.baz") |  | ||||||
|  |  | ||||||
|     assert counter == 1 |     assert counter == 1 | ||||||
|  |  | ||||||
| @@ -224,7 +225,7 @@ async def test_dispatch_signal_triggers_event_on_bp(app): | |||||||
|  |  | ||||||
|     app.blueprint(bp) |     app.blueprint(bp) | ||||||
|     app.signal_router.finalize() |     app.signal_router.finalize() | ||||||
|     signal, *_ = app.signal_router.get( |     signal_group, *_ = app.signal_router.get( | ||||||
|         "foo.bar.baz", condition={"blueprint": "bp"} |         "foo.bar.baz", condition={"blueprint": "bp"} | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
| @@ -233,7 +234,8 @@ async def test_dispatch_signal_triggers_event_on_bp(app): | |||||||
|     assert isawaitable(waiter) |     assert isawaitable(waiter) | ||||||
|  |  | ||||||
|     fut = asyncio.ensure_future(do_wait()) |     fut = asyncio.ensure_future(do_wait()) | ||||||
|     signal.ctx.event.set() |     for signal in signal_group: | ||||||
|  |         signal.ctx.event.set() | ||||||
|     await fut |     await fut | ||||||
|  |  | ||||||
|     assert bp_counter == 1 |     assert bp_counter == 1 | ||||||
|   | |||||||
| @@ -1,11 +1,16 @@ | |||||||
| import inspect | import inspect | ||||||
|  | import logging | ||||||
| import os | import os | ||||||
|  |  | ||||||
|  | from collections import Counter | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from time import gmtime, strftime | from time import gmtime, strftime | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
|  | from sanic import text | ||||||
|  | from sanic.exceptions import FileNotFound | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture(scope="module") | @pytest.fixture(scope="module") | ||||||
| def static_file_directory(): | def static_file_directory(): | ||||||
| @@ -454,3 +459,51 @@ def test_nested_dir(app, static_file_directory): | |||||||
|  |  | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.text == "foo\n" |     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