Compare commits
	
		
			6 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | df4970a73d | ||
|   | c5070bd449 | ||
|   | eb3d0a3f87 | ||
|   | c09129ec63 | ||
|   | 2a44a27236 | ||
|   | bb9ff7cec1 | 
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							| @@ -71,7 +71,7 @@ black: | ||||
| 	black --config ./.black.toml sanic tests | ||||
|  | ||||
| fix-import: black | ||||
| 	isort -rc sanic tests | ||||
| 	isort sanic tests | ||||
|  | ||||
|  | ||||
| docs-clean: | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| __version__ = "19.12.0" | ||||
| __version__ = "19.12.3" | ||||
|   | ||||
							
								
								
									
										57
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -194,6 +194,12 @@ class Sanic: | ||||
|             strict_slashes = self.strict_slashes | ||||
|  | ||||
|         def response(handler): | ||||
|             if isinstance(handler, tuple): | ||||
|                 # if a handler fn is already wrapped in a route, the handler | ||||
|                 # variable will be a tuple of (existing routes, handler fn) | ||||
|                 routes, handler = handler | ||||
|             else: | ||||
|                 routes = [] | ||||
|             args = list(signature(handler).parameters.keys()) | ||||
|  | ||||
|             if not args: | ||||
| @@ -205,14 +211,16 @@ class Sanic: | ||||
|             if stream: | ||||
|                 handler.is_stream = stream | ||||
|  | ||||
|             routes = self.router.add( | ||||
|                 uri=uri, | ||||
|                 methods=methods, | ||||
|                 handler=handler, | ||||
|                 host=host, | ||||
|                 strict_slashes=strict_slashes, | ||||
|                 version=version, | ||||
|                 name=name, | ||||
|             routes.extend( | ||||
|                 self.router.add( | ||||
|                     uri=uri, | ||||
|                     methods=methods, | ||||
|                     handler=handler, | ||||
|                     host=host, | ||||
|                     strict_slashes=strict_slashes, | ||||
|                     version=version, | ||||
|                     name=name, | ||||
|                 ) | ||||
|             ) | ||||
|             return routes, handler | ||||
|  | ||||
| @@ -476,6 +484,13 @@ class Sanic: | ||||
|             strict_slashes = self.strict_slashes | ||||
|  | ||||
|         def response(handler): | ||||
|             if isinstance(handler, tuple): | ||||
|                 # if a handler fn is already wrapped in a route, the handler | ||||
|                 # variable will be a tuple of (existing routes, handler fn) | ||||
|                 routes, handler = handler | ||||
|             else: | ||||
|                 routes = [] | ||||
|  | ||||
|             async def websocket_handler(request, *args, **kwargs): | ||||
|                 request.app = self | ||||
|                 if not getattr(handler, "__blueprintname__", False): | ||||
| @@ -516,13 +531,15 @@ class Sanic: | ||||
|                     self.websocket_tasks.remove(fut) | ||||
|                 await ws.close() | ||||
|  | ||||
|             routes = self.router.add( | ||||
|                 uri=uri, | ||||
|                 handler=websocket_handler, | ||||
|                 methods=frozenset({"GET"}), | ||||
|                 host=host, | ||||
|                 strict_slashes=strict_slashes, | ||||
|                 name=name, | ||||
|             routes.extend( | ||||
|                 self.router.add( | ||||
|                     uri=uri, | ||||
|                     handler=websocket_handler, | ||||
|                     methods=frozenset({"GET"}), | ||||
|                     host=host, | ||||
|                     strict_slashes=strict_slashes, | ||||
|                     name=name, | ||||
|                 ) | ||||
|             ) | ||||
|             return routes, handler | ||||
|  | ||||
| @@ -813,6 +830,14 @@ class Sanic: | ||||
|                 "Endpoint with name `{}` was not found".format(view_name) | ||||
|             ) | ||||
|  | ||||
|         # If the route has host defined, split that off | ||||
|         # TODO: Retain netloc and path separately in Route objects | ||||
|         host = uri.find("/") | ||||
|         if host > 0: | ||||
|             host, uri = uri[:host], uri[host:] | ||||
|         else: | ||||
|             host = None | ||||
|  | ||||
|         if view_name == "static" or view_name.endswith(".static"): | ||||
|             filename = kwargs.pop("filename", None) | ||||
|             # it's static folder | ||||
| @@ -845,7 +870,7 @@ class Sanic: | ||||
|  | ||||
|         netloc = kwargs.pop("_server", None) | ||||
|         if netloc is None and external: | ||||
|             netloc = self.config.get("SERVER_NAME", "") | ||||
|             netloc = host or self.config.get("SERVER_NAME", "") | ||||
|  | ||||
|         if external: | ||||
|             if not scheme: | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import asyncio | ||||
| import warnings | ||||
|  | ||||
| from inspect import isawaitable | ||||
| from typing import ( | ||||
|     Any, | ||||
| @@ -16,7 +15,6 @@ from typing import ( | ||||
| from urllib.parse import quote | ||||
|  | ||||
| import sanic.app  # noqa | ||||
|  | ||||
| from sanic.compat import Header | ||||
| from sanic.exceptions import InvalidUsage, ServerError | ||||
| from sanic.log import logger | ||||
| @@ -25,7 +23,6 @@ from sanic.response import HTTPResponse, StreamingHTTPResponse | ||||
| from sanic.server import StreamBuffer | ||||
| from sanic.websocket import WebSocketConnection | ||||
|  | ||||
|  | ||||
| ASGIScope = MutableMapping[str, Any] | ||||
| ASGIMessage = MutableMapping[str, Any] | ||||
| ASGISend = Callable[[ASGIMessage], Awaitable[None]] | ||||
| @@ -68,9 +65,7 @@ class MockProtocol: | ||||
| class MockTransport: | ||||
|     _protocol: Optional[MockProtocol] | ||||
|  | ||||
|     def __init__( | ||||
|         self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend | ||||
|     ) -> None: | ||||
|     def __init__(self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend) -> None: | ||||
|         self.scope = scope | ||||
|         self._receive = receive | ||||
|         self._send = send | ||||
| @@ -146,9 +141,7 @@ class Lifespan: | ||||
|         ) + self.asgi_app.sanic_app.listeners.get("after_server_start", []) | ||||
|  | ||||
|         for handler in listeners: | ||||
|             response = handler( | ||||
|                 self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop | ||||
|             ) | ||||
|             response = handler(self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop) | ||||
|             if isawaitable(response): | ||||
|                 await response | ||||
|  | ||||
| @@ -166,9 +159,7 @@ class Lifespan: | ||||
|         ) + self.asgi_app.sanic_app.listeners.get("after_server_stop", []) | ||||
|  | ||||
|         for handler in listeners: | ||||
|             response = handler( | ||||
|                 self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop | ||||
|             ) | ||||
|             response = handler(self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop) | ||||
|             if isawaitable(response): | ||||
|                 await response | ||||
|  | ||||
| @@ -213,19 +204,13 @@ class ASGIApp: | ||||
|                 for key, value in scope.get("headers", []) | ||||
|             ] | ||||
|         ) | ||||
|         instance.do_stream = ( | ||||
|             True if headers.get("expect") == "100-continue" else False | ||||
|         ) | ||||
|         instance.do_stream = True if headers.get("expect") == "100-continue" else False | ||||
|         instance.lifespan = Lifespan(instance) | ||||
|  | ||||
|         if scope["type"] == "lifespan": | ||||
|             await instance.lifespan(scope, receive, send) | ||||
|         else: | ||||
|             path = ( | ||||
|                 scope["path"][1:] | ||||
|                 if scope["path"].startswith("/") | ||||
|                 else scope["path"] | ||||
|             ) | ||||
|             path = scope["path"][1:] if scope["path"].startswith("/") else scope["path"] | ||||
|             url = "/".join([scope.get("root_path", ""), quote(path)]) | ||||
|             url_bytes = url.encode("latin-1") | ||||
|             url_bytes += b"?" + scope["query_string"] | ||||
| @@ -248,18 +233,11 @@ class ASGIApp: | ||||
|  | ||||
|             request_class = sanic_app.request_class or Request | ||||
|             instance.request = request_class( | ||||
|                 url_bytes, | ||||
|                 headers, | ||||
|                 version, | ||||
|                 method, | ||||
|                 instance.transport, | ||||
|                 sanic_app, | ||||
|                 url_bytes, headers, version, method, instance.transport, sanic_app, | ||||
|             ) | ||||
|  | ||||
|             if sanic_app.is_request_stream: | ||||
|                 is_stream_handler = sanic_app.router.is_stream_handler( | ||||
|                     instance.request | ||||
|                 ) | ||||
|                 is_stream_handler = sanic_app.router.is_stream_handler(instance.request) | ||||
|                 if is_stream_handler: | ||||
|                     instance.request.stream = StreamBuffer( | ||||
|                         sanic_app.config.REQUEST_BUFFER_QUEUE_SIZE | ||||
| @@ -338,26 +316,22 @@ class ASGIApp: | ||||
|                 type(response), | ||||
|             ) | ||||
|             exception = ServerError("Invalid response type") | ||||
|             response = self.sanic_app.error_handler.response( | ||||
|                 self.request, exception | ||||
|             ) | ||||
|             response = self.sanic_app.error_handler.response(self.request, exception) | ||||
|             headers = [ | ||||
|                 (str(name).encode("latin-1"), str(value).encode("latin-1")) | ||||
|                 for name, value in response.headers.items() | ||||
|                 if name not in (b"Set-Cookie",) | ||||
|             ] | ||||
|  | ||||
|         response.asgi = True | ||||
|  | ||||
|         if "content-length" not in response.headers and not isinstance( | ||||
|             response, StreamingHTTPResponse | ||||
|         ): | ||||
|             headers += [ | ||||
|                 (b"content-length", str(len(response.body)).encode("latin-1")) | ||||
|             ] | ||||
|             headers += [(b"content-length", str(len(response.body)).encode("latin-1"))] | ||||
|  | ||||
|         if "content-type" not in response.headers: | ||||
|             headers += [ | ||||
|                 (b"content-type", str(response.content_type).encode("latin-1")) | ||||
|             ] | ||||
|             headers += [(b"content-type", str(response.content_type).encode("latin-1"))] | ||||
|  | ||||
|         if response.cookies: | ||||
|             cookies.update( | ||||
| @@ -369,8 +343,7 @@ class ASGIApp: | ||||
|             ) | ||||
|  | ||||
|         headers += [ | ||||
|             (b"set-cookie", cookie.encode("utf-8")) | ||||
|             for k, cookie in cookies.items() | ||||
|             (b"set-cookie", cookie.encode("utf-8")) for k, cookie in cookies.items() | ||||
|         ] | ||||
|  | ||||
|         await self.transport.send( | ||||
|   | ||||
| @@ -129,27 +129,27 @@ class Request: | ||||
|  | ||||
|     def get(self, key, default=None): | ||||
|         """.. deprecated:: 19.9 | ||||
|            Custom context is now stored in `request.custom_context.yourkey`""" | ||||
|         Custom context is now stored in `request.custom_context.yourkey`""" | ||||
|         return self.ctx.__dict__.get(key, default) | ||||
|  | ||||
|     def __contains__(self, key): | ||||
|         """.. deprecated:: 19.9 | ||||
|            Custom context is now stored in `request.custom_context.yourkey`""" | ||||
|         Custom context is now stored in `request.custom_context.yourkey`""" | ||||
|         return key in self.ctx.__dict__ | ||||
|  | ||||
|     def __getitem__(self, key): | ||||
|         """.. deprecated:: 19.9 | ||||
|            Custom context is now stored in `request.custom_context.yourkey`""" | ||||
|         Custom context is now stored in `request.custom_context.yourkey`""" | ||||
|         return self.ctx.__dict__[key] | ||||
|  | ||||
|     def __delitem__(self, key): | ||||
|         """.. deprecated:: 19.9 | ||||
|            Custom context is now stored in `request.custom_context.yourkey`""" | ||||
|         Custom context is now stored in `request.custom_context.yourkey`""" | ||||
|         del self.ctx.__dict__[key] | ||||
|  | ||||
|     def __setitem__(self, key, value): | ||||
|         """.. deprecated:: 19.9 | ||||
|            Custom context is now stored in `request.custom_context.yourkey`""" | ||||
|         Custom context is now stored in `request.custom_context.yourkey`""" | ||||
|         setattr(self.ctx, key, value) | ||||
|  | ||||
|     def body_init(self): | ||||
|   | ||||
| @@ -22,6 +22,9 @@ except ImportError: | ||||
|  | ||||
|  | ||||
| class BaseHTTPResponse: | ||||
|     def __init__(self): | ||||
|         self.asgi = False | ||||
|  | ||||
|     def _encode_body(self, data): | ||||
|         try: | ||||
|             # Try to encode it regularly | ||||
| @@ -59,12 +62,15 @@ class StreamingHTTPResponse(BaseHTTPResponse): | ||||
|         content_type="text/plain", | ||||
|         chunked=True, | ||||
|     ): | ||||
|         super().__init__() | ||||
|  | ||||
|         self.content_type = content_type | ||||
|         self.streaming_fn = streaming_fn | ||||
|         self.status = status | ||||
|         self.headers = Header(headers or {}) | ||||
|         self.chunked = chunked | ||||
|         self._cookies = None | ||||
|         self.protocol = None | ||||
|  | ||||
|     async def write(self, data): | ||||
|         """Writes a chunk of data to the streaming response. | ||||
| @@ -93,8 +99,9 @@ class StreamingHTTPResponse(BaseHTTPResponse): | ||||
|             keep_alive=keep_alive, | ||||
|             keep_alive_timeout=keep_alive_timeout, | ||||
|         ) | ||||
|         await self.protocol.push_data(headers) | ||||
|         await self.protocol.drain() | ||||
|         if not getattr(self, "asgi", False): | ||||
|             await self.protocol.push_data(headers) | ||||
|             await self.protocol.drain() | ||||
|         await self.streaming_fn(self) | ||||
|         if self.chunked: | ||||
|             await self.protocol.push_data(b"0\r\n\r\n") | ||||
| @@ -144,6 +151,8 @@ class HTTPResponse(BaseHTTPResponse): | ||||
|         content_type=None, | ||||
|         body_bytes=b"", | ||||
|     ): | ||||
|         super().__init__() | ||||
|  | ||||
|         self.content_type = content_type | ||||
|  | ||||
|         if body is not None: | ||||
| @@ -202,16 +211,14 @@ class HTTPResponse(BaseHTTPResponse): | ||||
|         return self._cookies | ||||
|  | ||||
|  | ||||
| def empty( | ||||
|     status=204, headers=None, | ||||
| ): | ||||
| def empty(status=204, headers=None): | ||||
|     """ | ||||
|     Returns an empty response to the client. | ||||
|  | ||||
|     :param status Response code. | ||||
|     :param headers Custom Headers. | ||||
|     """ | ||||
|     return HTTPResponse(body_bytes=b"", status=status, headers=headers,) | ||||
|     return HTTPResponse(body_bytes=b"", status=status, headers=headers) | ||||
|  | ||||
|  | ||||
| def json( | ||||
|   | ||||
| @@ -484,7 +484,7 @@ class Router: | ||||
|         return route_handler, [], kwargs, route.uri, route.name | ||||
|  | ||||
|     def is_stream_handler(self, request): | ||||
|         """ Handler for request is stream or not. | ||||
|         """Handler for request is stream or not. | ||||
|         :param request: Request object | ||||
|         :return: bool | ||||
|         """ | ||||
|   | ||||
| @@ -731,6 +731,26 @@ class AsyncioServer: | ||||
|             task = asyncio.ensure_future(coro, loop=self.loop) | ||||
|             return task | ||||
|  | ||||
|     def start_serving(self): | ||||
|         if self.server: | ||||
|             try: | ||||
|                 return self.server.start_serving() | ||||
|             except AttributeError: | ||||
|                 raise NotImplementedError( | ||||
|                     "server.start_serving not available in this version " | ||||
|                     "of asyncio or uvloop." | ||||
|                 ) | ||||
|  | ||||
|     def serve_forever(self): | ||||
|         if self.server: | ||||
|             try: | ||||
|                 return self.server.serve_forever() | ||||
|             except AttributeError: | ||||
|                 raise NotImplementedError( | ||||
|                     "server.serve_forever not available in this version " | ||||
|                     "of asyncio or uvloop." | ||||
|                 ) | ||||
|  | ||||
|     def __await__(self): | ||||
|         """Starts the asyncio server, returns AsyncServerCoro""" | ||||
|         task = asyncio.ensure_future(self.serve_coro) | ||||
|   | ||||
| @@ -174,7 +174,7 @@ class GunicornWorker(base.Worker): | ||||
|  | ||||
|     @staticmethod | ||||
|     def _create_ssl_context(cfg): | ||||
|         """ Creates SSLContext instance for usage in asyncio.create_server. | ||||
|         """Creates SSLContext instance for usage in asyncio.create_server. | ||||
|         See ssl.SSLSocket.__init__ for more details. | ||||
|         """ | ||||
|         ctx = ssl.SSLContext(cfg.ssl_version) | ||||
|   | ||||
							
								
								
									
										13
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								setup.py
									
									
									
									
									
								
							| @@ -5,7 +5,6 @@ import codecs | ||||
| import os | ||||
| import re | ||||
| import sys | ||||
|  | ||||
| from distutils.util import strtobool | ||||
|  | ||||
| from setuptools import setup | ||||
| @@ -39,9 +38,7 @@ def open_local(paths, mode="r", encoding="utf8"): | ||||
|  | ||||
| with open_local(["sanic", "__version__.py"], encoding="latin1") as fp: | ||||
|     try: | ||||
|         version = re.findall( | ||||
|             r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M | ||||
|         )[0] | ||||
|         version = re.findall(r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M)[0] | ||||
|     except IndexError: | ||||
|         raise RuntimeError("Unable to determine version.") | ||||
|  | ||||
| @@ -71,9 +68,7 @@ setup_kwargs = { | ||||
|     ], | ||||
| } | ||||
|  | ||||
| env_dependency = ( | ||||
|     '; sys_platform != "win32" ' 'and implementation_name == "cpython"' | ||||
| ) | ||||
| env_dependency = '; sys_platform != "win32" ' 'and implementation_name == "cpython"' | ||||
| ujson = "ujson>=1.35" + env_dependency | ||||
| uvloop = "uvloop>=0.5.3" + env_dependency | ||||
|  | ||||
| @@ -83,13 +78,13 @@ requirements = [ | ||||
|     ujson, | ||||
|     "aiofiles>=0.3.0", | ||||
|     "websockets>=7.0,<9.0", | ||||
|     "multidict>=4.0,<5.0", | ||||
|     "multidict==5.0.0", | ||||
|     "httpx==0.9.3", | ||||
| ] | ||||
|  | ||||
| tests_require = [ | ||||
|     "pytest==5.2.1", | ||||
|     "multidict>=4.0,<5.0", | ||||
|     "multidict==5.0.0", | ||||
|     "gunicorn", | ||||
|     "pytest-cov", | ||||
|     "httpcore==0.3.0", | ||||
|   | ||||
| @@ -39,6 +39,7 @@ main = WSGIApplication( | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     import sys | ||||
|  | ||||
|     from wsgiref.simple_server import make_server | ||||
|  | ||||
|     try: | ||||
|   | ||||
| @@ -41,6 +41,20 @@ def test_create_asyncio_server(app): | ||||
|         assert srv.is_serving() is True | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif( | ||||
|     sys.version_info < (3, 7), reason="requires python3.7 or higher" | ||||
| ) | ||||
| def test_asyncio_server_no_start_serving(app): | ||||
|     if not uvloop_installed(): | ||||
|         loop = asyncio.get_event_loop() | ||||
|         asyncio_srv_coro = app.create_server( | ||||
|             return_asyncio_server=True, | ||||
|             asyncio_server_kwargs=dict(start_serving=False), | ||||
|         ) | ||||
|         srv = loop.run_until_complete(asyncio_srv_coro) | ||||
|         assert srv.is_serving() is False | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif( | ||||
|     sys.version_info < (3, 7), reason="requires python3.7 or higher" | ||||
| ) | ||||
| @@ -53,6 +67,10 @@ def test_asyncio_server_start_serving(app): | ||||
|         ) | ||||
|         srv = loop.run_until_complete(asyncio_srv_coro) | ||||
|         assert srv.is_serving() is False | ||||
|         loop.run_until_complete(srv.start_serving()) | ||||
|         assert srv.is_serving() is True | ||||
|         srv.close() | ||||
|         # Looks like we can't easily test `serve_forever()` | ||||
|  | ||||
|  | ||||
| def test_app_loop_not_running(app): | ||||
|   | ||||
| @@ -230,8 +230,8 @@ async def handler3(request): | ||||
|  | ||||
| def test_keep_alive_timeout_reuse(): | ||||
|     """If the server keep-alive timeout and client keep-alive timeout are | ||||
|      both longer than the delay, the client _and_ server will successfully | ||||
|      reuse the existing connection.""" | ||||
|     both longer than the delay, the client _and_ server will successfully | ||||
|     reuse the existing connection.""" | ||||
|     try: | ||||
|         loop = asyncio.new_event_loop() | ||||
|         asyncio.set_event_loop(loop) | ||||
|   | ||||
| @@ -15,13 +15,13 @@ from aiofiles import os as async_os | ||||
| from sanic.response import ( | ||||
|     HTTPResponse, | ||||
|     StreamingHTTPResponse, | ||||
|     empty, | ||||
|     file, | ||||
|     file_stream, | ||||
|     json, | ||||
|     raw, | ||||
|     stream, | ||||
| ) | ||||
| from sanic.response import empty | ||||
| from sanic.server import HttpProtocol | ||||
| from sanic.testing import HOST, PORT | ||||
|  | ||||
| @@ -232,6 +232,12 @@ def test_chunked_streaming_returns_correct_content(streaming_app): | ||||
|     assert response.text == "foo,bar" | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_chunked_streaming_returns_correct_content_asgi(streaming_app): | ||||
|     request, response = await streaming_app.asgi_client.get("/") | ||||
|     assert response.text == "4\r\nfoo,\r\n3\r\nbar\r\n0\r\n\r\n" | ||||
|  | ||||
|  | ||||
| def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): | ||||
|     request, response = non_chunked_streaming_app.test_client.get("/") | ||||
|     assert "Transfer-Encoding" not in response.headers | ||||
| @@ -239,8 +245,18 @@ def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): | ||||
|     assert response.headers["Content-Length"] == "7" | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_non_chunked_streaming_adds_correct_headers_asgi( | ||||
|     non_chunked_streaming_app, | ||||
| ): | ||||
|     request, response = await non_chunked_streaming_app.asgi_client.get("/") | ||||
|     assert "Transfer-Encoding" not in response.headers | ||||
|     assert response.headers["Content-Type"] == "text/csv" | ||||
|     assert response.headers["Content-Length"] == "7" | ||||
|  | ||||
|  | ||||
| def test_non_chunked_streaming_returns_correct_content( | ||||
|     non_chunked_streaming_app | ||||
|     non_chunked_streaming_app, | ||||
| ): | ||||
|     request, response = non_chunked_streaming_app.test_client.get("/") | ||||
|     assert response.text == "foo,bar" | ||||
| @@ -255,7 +271,7 @@ def test_stream_response_status_returns_correct_headers(status): | ||||
|  | ||||
| @pytest.mark.parametrize("keep_alive_timeout", [10, 20, 30]) | ||||
| def test_stream_response_keep_alive_returns_correct_headers( | ||||
|     keep_alive_timeout | ||||
|     keep_alive_timeout, | ||||
| ): | ||||
|     response = StreamingHTTPResponse(sample_streaming_fn) | ||||
|     headers = response.get_headers( | ||||
| @@ -284,7 +300,7 @@ def test_stream_response_does_not_include_chunked_header_if_disabled(): | ||||
|  | ||||
|  | ||||
| def test_stream_response_writes_correct_content_to_transport_when_chunked( | ||||
|     streaming_app | ||||
|     streaming_app, | ||||
| ): | ||||
|     response = StreamingHTTPResponse(sample_streaming_fn) | ||||
|     response.protocol = MagicMock(HttpProtocol) | ||||
|   | ||||
| @@ -551,6 +551,35 @@ def test_route_duplicate(app): | ||||
|             pass | ||||
|  | ||||
|  | ||||
| def test_double_stack_route(app): | ||||
|     @app.route("/test/1") | ||||
|     @app.route("/test/2") | ||||
|     async def handler1(request): | ||||
|         return text("OK") | ||||
|  | ||||
|     request, response = app.test_client.get("/test/1") | ||||
|     assert response.status == 200 | ||||
|     request, response = app.test_client.get("/test/2") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_websocket_route_asgi(app): | ||||
|     ev = asyncio.Event() | ||||
|  | ||||
|     @app.websocket("/test/1") | ||||
|     @app.websocket("/test/2") | ||||
|     async def handler(request, ws): | ||||
|         ev.set() | ||||
|  | ||||
|     request, response = await app.asgi_client.websocket("/test/1") | ||||
|     first_set = ev.is_set() | ||||
|     ev.clear() | ||||
|     request, response = await app.asgi_client.websocket("/test/1") | ||||
|     second_set = ev.is_set() | ||||
|     assert first_set and second_set | ||||
|  | ||||
|  | ||||
| def test_method_not_allowed(app): | ||||
|     @app.route("/test", methods=["GET"]) | ||||
|     async def handler(request): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user