Compare commits
	
		
			11 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 9b24fbb2f3 | ||
|   | 468f4ac7f1 | ||
|   | be1ca93a23 | ||
|   | 662c7c7f62 | ||
|   | 3e4bec7f2c | ||
|   | df4970a73d | ||
|   | c5070bd449 | ||
|   | eb3d0a3f87 | ||
|   | c09129ec63 | ||
|   | 2a44a27236 | ||
|   | bb9ff7cec1 | 
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							| @@ -71,7 +71,7 @@ black: | |||||||
| 	black --config ./.black.toml sanic tests | 	black --config ./.black.toml sanic tests | ||||||
|  |  | ||||||
| fix-import: black | fix-import: black | ||||||
| 	isort -rc sanic tests | 	isort sanic tests | ||||||
|  |  | ||||||
|  |  | ||||||
| docs-clean: | docs-clean: | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| __version__ = "19.12.0" | __version__ = "19.12.4" | ||||||
|   | |||||||
							
								
								
									
										59
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -194,6 +194,12 @@ class Sanic: | |||||||
|             strict_slashes = self.strict_slashes |             strict_slashes = self.strict_slashes | ||||||
|  |  | ||||||
|         def response(handler): |         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()) |             args = list(signature(handler).parameters.keys()) | ||||||
|  |  | ||||||
|             if not args: |             if not args: | ||||||
| @@ -205,14 +211,16 @@ class Sanic: | |||||||
|             if stream: |             if stream: | ||||||
|                 handler.is_stream = stream |                 handler.is_stream = stream | ||||||
|  |  | ||||||
|             routes = self.router.add( |             routes.extend( | ||||||
|                 uri=uri, |                 self.router.add( | ||||||
|                 methods=methods, |                     uri=uri, | ||||||
|                 handler=handler, |                     methods=methods, | ||||||
|                 host=host, |                     handler=handler, | ||||||
|                 strict_slashes=strict_slashes, |                     host=host, | ||||||
|                 version=version, |                     strict_slashes=strict_slashes, | ||||||
|                 name=name, |                     version=version, | ||||||
|  |                     name=name, | ||||||
|  |                 ) | ||||||
|             ) |             ) | ||||||
|             return routes, handler |             return routes, handler | ||||||
|  |  | ||||||
| @@ -476,6 +484,13 @@ class Sanic: | |||||||
|             strict_slashes = self.strict_slashes |             strict_slashes = self.strict_slashes | ||||||
|  |  | ||||||
|         def response(handler): |         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): |             async def websocket_handler(request, *args, **kwargs): | ||||||
|                 request.app = self |                 request.app = self | ||||||
|                 if not getattr(handler, "__blueprintname__", False): |                 if not getattr(handler, "__blueprintname__", False): | ||||||
| @@ -516,13 +531,15 @@ class Sanic: | |||||||
|                     self.websocket_tasks.remove(fut) |                     self.websocket_tasks.remove(fut) | ||||||
|                 await ws.close() |                 await ws.close() | ||||||
|  |  | ||||||
|             routes = self.router.add( |             routes.extend( | ||||||
|                 uri=uri, |                 self.router.add( | ||||||
|                 handler=websocket_handler, |                     uri=uri, | ||||||
|                 methods=frozenset({"GET"}), |                     handler=websocket_handler, | ||||||
|                 host=host, |                     methods=frozenset({"GET"}), | ||||||
|                 strict_slashes=strict_slashes, |                     host=host, | ||||||
|                 name=name, |                     strict_slashes=strict_slashes, | ||||||
|  |                     name=name, | ||||||
|  |                 ) | ||||||
|             ) |             ) | ||||||
|             return routes, handler |             return routes, handler | ||||||
|  |  | ||||||
| @@ -813,6 +830,14 @@ class Sanic: | |||||||
|                 "Endpoint with name `{}` was not found".format(view_name) |                 "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"): |         if view_name == "static" or view_name.endswith(".static"): | ||||||
|             filename = kwargs.pop("filename", None) |             filename = kwargs.pop("filename", None) | ||||||
|             # it's static folder |             # it's static folder | ||||||
| @@ -845,7 +870,7 @@ class Sanic: | |||||||
|  |  | ||||||
|         netloc = kwargs.pop("_server", None) |         netloc = kwargs.pop("_server", None) | ||||||
|         if netloc is None and external: |         if netloc is None and external: | ||||||
|             netloc = self.config.get("SERVER_NAME", "") |             netloc = host or self.config.get("SERVER_NAME", "") | ||||||
|  |  | ||||||
|         if external: |         if external: | ||||||
|             if not scheme: |             if not scheme: | ||||||
| @@ -1449,3 +1474,5 @@ class Sanic: | |||||||
|         self.asgi = True |         self.asgi = True | ||||||
|         asgi_app = await ASGIApp.create(self, scope, receive, send) |         asgi_app = await ASGIApp.create(self, scope, receive, send) | ||||||
|         await asgi_app() |         await asgi_app() | ||||||
|  |  | ||||||
|  |     _asgi_single_callable = True  # We conform to ASGI 3.0 single-callable | ||||||
|   | |||||||
| @@ -309,13 +309,17 @@ class ASGIApp: | |||||||
|         callback = None if self.ws else self.stream_callback |         callback = None if self.ws else self.stream_callback | ||||||
|         await handler(self.request, None, callback) |         await handler(self.request, None, callback) | ||||||
|  |  | ||||||
|  |     _asgi_single_callable = True  # We conform to ASGI 3.0 single-callable | ||||||
|  |  | ||||||
|     async def stream_callback(self, response: HTTPResponse) -> None: |     async def stream_callback(self, response: HTTPResponse) -> None: | ||||||
|         """ |         """ | ||||||
|         Write the response. |         Write the response. | ||||||
|         """ |         """ | ||||||
|         headers: List[Tuple[bytes, bytes]] = [] |         headers: List[Tuple[bytes, bytes]] = [] | ||||||
|         cookies: Dict[str, str] = {} |         cookies: Dict[str, str] = {} | ||||||
|  |         content_length: List[str] = [] | ||||||
|         try: |         try: | ||||||
|  |             content_length = response.headers.popall("content-length", []) | ||||||
|             cookies = { |             cookies = { | ||||||
|                 v.key: v |                 v.key: v | ||||||
|                 for _, v in list( |                 for _, v in list( | ||||||
| @@ -347,11 +351,23 @@ class ASGIApp: | |||||||
|                 if name not in (b"Set-Cookie",) |                 if name not in (b"Set-Cookie",) | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|         if "content-length" not in response.headers and not isinstance( |         response.asgi = True | ||||||
|             response, StreamingHTTPResponse |         is_streaming = isinstance(response, StreamingHTTPResponse) | ||||||
|         ): |         if is_streaming and getattr(response, "chunked", False): | ||||||
|  |             # disable sanic chunking, this is done at the ASGI-server level | ||||||
|  |             setattr(response, "chunked", False) | ||||||
|  |             # content-length header is removed to signal to the ASGI-server | ||||||
|  |             # to use automatic-chunking if it supports it | ||||||
|  |         elif len(content_length) > 0: | ||||||
|             headers += [ |             headers += [ | ||||||
|                 (b"content-length", str(len(response.body)).encode("latin-1")) |                 (b"content-length", str(content_length[0]).encode("latin-1")) | ||||||
|  |             ] | ||||||
|  |         elif not is_streaming: | ||||||
|  |             headers += [ | ||||||
|  |                 ( | ||||||
|  |                     b"content-length", | ||||||
|  |                     str(len(getattr(response, "body", b""))).encode("latin-1"), | ||||||
|  |                 ) | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|         if "content-type" not in response.headers: |         if "content-type" not in response.headers: | ||||||
|   | |||||||
| @@ -129,27 +129,27 @@ class Request: | |||||||
|  |  | ||||||
|     def get(self, key, default=None): |     def get(self, key, default=None): | ||||||
|         """.. deprecated:: 19.9 |         """.. 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) |         return self.ctx.__dict__.get(key, default) | ||||||
|  |  | ||||||
|     def __contains__(self, key): |     def __contains__(self, key): | ||||||
|         """.. deprecated:: 19.9 |         """.. 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__ |         return key in self.ctx.__dict__ | ||||||
|  |  | ||||||
|     def __getitem__(self, key): |     def __getitem__(self, key): | ||||||
|         """.. deprecated:: 19.9 |         """.. 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] |         return self.ctx.__dict__[key] | ||||||
|  |  | ||||||
|     def __delitem__(self, key): |     def __delitem__(self, key): | ||||||
|         """.. deprecated:: 19.9 |         """.. 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] |         del self.ctx.__dict__[key] | ||||||
|  |  | ||||||
|     def __setitem__(self, key, value): |     def __setitem__(self, key, value): | ||||||
|         """.. deprecated:: 19.9 |         """.. 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) |         setattr(self.ctx, key, value) | ||||||
|  |  | ||||||
|     def body_init(self): |     def body_init(self): | ||||||
|   | |||||||
| @@ -22,6 +22,9 @@ except ImportError: | |||||||
|  |  | ||||||
|  |  | ||||||
| class BaseHTTPResponse: | class BaseHTTPResponse: | ||||||
|  |     def __init__(self): | ||||||
|  |         self.asgi = False | ||||||
|  |  | ||||||
|     def _encode_body(self, data): |     def _encode_body(self, data): | ||||||
|         try: |         try: | ||||||
|             # Try to encode it regularly |             # Try to encode it regularly | ||||||
| @@ -59,12 +62,15 @@ class StreamingHTTPResponse(BaseHTTPResponse): | |||||||
|         content_type="text/plain", |         content_type="text/plain", | ||||||
|         chunked=True, |         chunked=True, | ||||||
|     ): |     ): | ||||||
|  |         super().__init__() | ||||||
|  |  | ||||||
|         self.content_type = content_type |         self.content_type = content_type | ||||||
|         self.streaming_fn = streaming_fn |         self.streaming_fn = streaming_fn | ||||||
|         self.status = status |         self.status = status | ||||||
|         self.headers = Header(headers or {}) |         self.headers = Header(headers or {}) | ||||||
|         self.chunked = chunked |         self.chunked = chunked | ||||||
|         self._cookies = None |         self._cookies = None | ||||||
|  |         self.protocol = None | ||||||
|  |  | ||||||
|     async def write(self, data): |     async def write(self, data): | ||||||
|         """Writes a chunk of data to the streaming response. |         """Writes a chunk of data to the streaming response. | ||||||
| @@ -74,6 +80,8 @@ class StreamingHTTPResponse(BaseHTTPResponse): | |||||||
|         if type(data) != bytes: |         if type(data) != bytes: | ||||||
|             data = self._encode_body(data) |             data = self._encode_body(data) | ||||||
|  |  | ||||||
|  |         # `chunked` will always be False in ASGI-mode, even if the underlying | ||||||
|  |         # ASGI Transport implements Chunked transport. That does it itself. | ||||||
|         if self.chunked: |         if self.chunked: | ||||||
|             await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data)) |             await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data)) | ||||||
|         else: |         else: | ||||||
| @@ -93,8 +101,9 @@ class StreamingHTTPResponse(BaseHTTPResponse): | |||||||
|             keep_alive=keep_alive, |             keep_alive=keep_alive, | ||||||
|             keep_alive_timeout=keep_alive_timeout, |             keep_alive_timeout=keep_alive_timeout, | ||||||
|         ) |         ) | ||||||
|         await self.protocol.push_data(headers) |         if not getattr(self, "asgi", False): | ||||||
|         await self.protocol.drain() |             await self.protocol.push_data(headers) | ||||||
|  |             await self.protocol.drain() | ||||||
|         await self.streaming_fn(self) |         await self.streaming_fn(self) | ||||||
|         if self.chunked: |         if self.chunked: | ||||||
|             await self.protocol.push_data(b"0\r\n\r\n") |             await self.protocol.push_data(b"0\r\n\r\n") | ||||||
| @@ -144,6 +153,8 @@ class HTTPResponse(BaseHTTPResponse): | |||||||
|         content_type=None, |         content_type=None, | ||||||
|         body_bytes=b"", |         body_bytes=b"", | ||||||
|     ): |     ): | ||||||
|  |         super().__init__() | ||||||
|  |  | ||||||
|         self.content_type = content_type |         self.content_type = content_type | ||||||
|  |  | ||||||
|         if body is not None: |         if body is not None: | ||||||
| @@ -202,16 +213,14 @@ class HTTPResponse(BaseHTTPResponse): | |||||||
|         return self._cookies |         return self._cookies | ||||||
|  |  | ||||||
|  |  | ||||||
| def empty( | def empty(status=204, headers=None): | ||||||
|     status=204, headers=None, |  | ||||||
| ): |  | ||||||
|     """ |     """ | ||||||
|     Returns an empty response to the client. |     Returns an empty response to the client. | ||||||
|  |  | ||||||
|     :param status Response code. |     :param status Response code. | ||||||
|     :param headers Custom Headers. |     :param headers Custom Headers. | ||||||
|     """ |     """ | ||||||
|     return HTTPResponse(body_bytes=b"", status=status, headers=headers,) |     return HTTPResponse(body_bytes=b"", status=status, headers=headers) | ||||||
|  |  | ||||||
|  |  | ||||||
| def json( | def json( | ||||||
|   | |||||||
| @@ -484,7 +484,7 @@ class Router: | |||||||
|         return route_handler, [], kwargs, route.uri, route.name |         return route_handler, [], kwargs, route.uri, route.name | ||||||
|  |  | ||||||
|     def is_stream_handler(self, request): |     def is_stream_handler(self, request): | ||||||
|         """ Handler for request is stream or not. |         """Handler for request is stream or not. | ||||||
|         :param request: Request object |         :param request: Request object | ||||||
|         :return: bool |         :return: bool | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -731,6 +731,26 @@ class AsyncioServer: | |||||||
|             task = asyncio.ensure_future(coro, loop=self.loop) |             task = asyncio.ensure_future(coro, loop=self.loop) | ||||||
|             return task |             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): |     def __await__(self): | ||||||
|         """Starts the asyncio server, returns AsyncServerCoro""" |         """Starts the asyncio server, returns AsyncServerCoro""" | ||||||
|         task = asyncio.ensure_future(self.serve_coro) |         task = asyncio.ensure_future(self.serve_coro) | ||||||
|   | |||||||
| @@ -174,7 +174,7 @@ class GunicornWorker(base.Worker): | |||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _create_ssl_context(cfg): |     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. |         See ssl.SSLSocket.__init__ for more details. | ||||||
|         """ |         """ | ||||||
|         ctx = ssl.SSLContext(cfg.ssl_version) |         ctx = ssl.SSLContext(cfg.ssl_version) | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								setup.py
									
									
									
									
									
								
							| @@ -5,7 +5,6 @@ import codecs | |||||||
| import os | import os | ||||||
| import re | import re | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
| from distutils.util import strtobool | from distutils.util import strtobool | ||||||
|  |  | ||||||
| from setuptools import setup | 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: | with open_local(["sanic", "__version__.py"], encoding="latin1") as fp: | ||||||
|     try: |     try: | ||||||
|         version = re.findall( |         version = re.findall(r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M)[0] | ||||||
|             r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M |  | ||||||
|         )[0] |  | ||||||
|     except IndexError: |     except IndexError: | ||||||
|         raise RuntimeError("Unable to determine version.") |         raise RuntimeError("Unable to determine version.") | ||||||
|  |  | ||||||
| @@ -71,9 +68,7 @@ setup_kwargs = { | |||||||
|     ], |     ], | ||||||
| } | } | ||||||
|  |  | ||||||
| env_dependency = ( | env_dependency = '; sys_platform != "win32" ' 'and implementation_name == "cpython"' | ||||||
|     '; sys_platform != "win32" ' 'and implementation_name == "cpython"' |  | ||||||
| ) |  | ||||||
| ujson = "ujson>=1.35" + env_dependency | ujson = "ujson>=1.35" + env_dependency | ||||||
| uvloop = "uvloop>=0.5.3" + env_dependency | uvloop = "uvloop>=0.5.3" + env_dependency | ||||||
|  |  | ||||||
| @@ -83,13 +78,13 @@ requirements = [ | |||||||
|     ujson, |     ujson, | ||||||
|     "aiofiles>=0.3.0", |     "aiofiles>=0.3.0", | ||||||
|     "websockets>=7.0,<9.0", |     "websockets>=7.0,<9.0", | ||||||
|     "multidict>=4.0,<5.0", |     "multidict==5.0.0", | ||||||
|     "httpx==0.9.3", |     "httpx==0.9.3", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| tests_require = [ | tests_require = [ | ||||||
|     "pytest==5.2.1", |     "pytest==5.2.1", | ||||||
|     "multidict>=4.0,<5.0", |     "multidict==5.0.0", | ||||||
|     "gunicorn", |     "gunicorn", | ||||||
|     "pytest-cov", |     "pytest-cov", | ||||||
|     "httpcore==0.3.0", |     "httpcore==0.3.0", | ||||||
|   | |||||||
| @@ -39,6 +39,7 @@ main = WSGIApplication( | |||||||
|  |  | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     import sys |     import sys | ||||||
|  |  | ||||||
|     from wsgiref.simple_server import make_server |     from wsgiref.simple_server import make_server | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|   | |||||||
| @@ -41,6 +41,20 @@ def test_create_asyncio_server(app): | |||||||
|         assert srv.is_serving() is True |         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( | @pytest.mark.skipif( | ||||||
|     sys.version_info < (3, 7), reason="requires python3.7 or higher" |     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) |         srv = loop.run_until_complete(asyncio_srv_coro) | ||||||
|         assert srv.is_serving() is False |         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): | def test_app_loop_not_running(app): | ||||||
|   | |||||||
| @@ -230,8 +230,8 @@ async def handler3(request): | |||||||
|  |  | ||||||
| def test_keep_alive_timeout_reuse(): | def test_keep_alive_timeout_reuse(): | ||||||
|     """If the server keep-alive timeout and client keep-alive timeout are |     """If the server keep-alive timeout and client keep-alive timeout are | ||||||
|      both longer than the delay, the client _and_ server will successfully |     both longer than the delay, the client _and_ server will successfully | ||||||
|      reuse the existing connection.""" |     reuse the existing connection.""" | ||||||
|     try: |     try: | ||||||
|         loop = asyncio.new_event_loop() |         loop = asyncio.new_event_loop() | ||||||
|         asyncio.set_event_loop(loop) |         asyncio.set_event_loop(loop) | ||||||
|   | |||||||
| @@ -15,13 +15,13 @@ from aiofiles import os as async_os | |||||||
| from sanic.response import ( | from sanic.response import ( | ||||||
|     HTTPResponse, |     HTTPResponse, | ||||||
|     StreamingHTTPResponse, |     StreamingHTTPResponse, | ||||||
|  |     empty, | ||||||
|     file, |     file, | ||||||
|     file_stream, |     file_stream, | ||||||
|     json, |     json, | ||||||
|     raw, |     raw, | ||||||
|     stream, |     stream, | ||||||
| ) | ) | ||||||
| from sanic.response import empty |  | ||||||
| from sanic.server import HttpProtocol | from sanic.server import HttpProtocol | ||||||
| from sanic.testing import HOST, PORT | from sanic.testing import HOST, PORT | ||||||
|  |  | ||||||
| @@ -232,6 +232,12 @@ def test_chunked_streaming_returns_correct_content(streaming_app): | |||||||
|     assert response.text == "foo,bar" |     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 == "foo,bar" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): | def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): | ||||||
|     request, response = non_chunked_streaming_app.test_client.get("/") |     request, response = non_chunked_streaming_app.test_client.get("/") | ||||||
|     assert "Transfer-Encoding" not in response.headers |     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" |     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( | 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("/") |     request, response = non_chunked_streaming_app.test_client.get("/") | ||||||
|     assert response.text == "foo,bar" |     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]) | @pytest.mark.parametrize("keep_alive_timeout", [10, 20, 30]) | ||||||
| def test_stream_response_keep_alive_returns_correct_headers( | def test_stream_response_keep_alive_returns_correct_headers( | ||||||
|     keep_alive_timeout |     keep_alive_timeout, | ||||||
| ): | ): | ||||||
|     response = StreamingHTTPResponse(sample_streaming_fn) |     response = StreamingHTTPResponse(sample_streaming_fn) | ||||||
|     headers = response.get_headers( |     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( | def test_stream_response_writes_correct_content_to_transport_when_chunked( | ||||||
|     streaming_app |     streaming_app, | ||||||
| ): | ): | ||||||
|     response = StreamingHTTPResponse(sample_streaming_fn) |     response = StreamingHTTPResponse(sample_streaming_fn) | ||||||
|     response.protocol = MagicMock(HttpProtocol) |     response.protocol = MagicMock(HttpProtocol) | ||||||
|   | |||||||
| @@ -551,6 +551,35 @@ def test_route_duplicate(app): | |||||||
|             pass |             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): | def test_method_not_allowed(app): | ||||||
|     @app.route("/test", methods=["GET"]) |     @app.route("/test", methods=["GET"]) | ||||||
|     async def handler(request): |     async def handler(request): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user