Merge branch 'master' into py39
This commit is contained in:
		| @@ -1,3 +1,28 @@ | ||||
| Version 20.9.1 | ||||
| =============== | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
|    | ||||
|   * | ||||
|     `#1954 <https://github.com/huge-success/sanic/pull/1954>`_ | ||||
|     Fix static route registration on blueprints | ||||
|   * | ||||
|     `#1957 <https://github.com/huge-success/sanic/pull/1957>`_ | ||||
|     Removes duplicate headers in ASGI streaming body | ||||
|  | ||||
|  | ||||
| Version 19.12.3 | ||||
| =============== | ||||
|  | ||||
| Bugfixes | ||||
| ******** | ||||
|    | ||||
|   * | ||||
|     `#1959 <https://github.com/huge-success/sanic/pull/1959>`_ | ||||
|     Removes duplicate headers in ASGI streaming body | ||||
|  | ||||
|  | ||||
| Version 20.9.0 | ||||
| =============== | ||||
|  | ||||
|   | ||||
| @@ -26,8 +26,8 @@ Sanic | Build fast. Run fast. | ||||
|    :target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge | ||||
| .. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg | ||||
|     :target: https://codecov.io/gh/huge-success/sanic | ||||
| .. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master | ||||
|    :target: https://travis-ci.org/huge-success/sanic | ||||
| .. |Build Status| image:: https://travis-ci.com/huge-success/sanic.svg?branch=master | ||||
|    :target: https://travis-ci.com/huge-success/sanic | ||||
| .. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true | ||||
|    :target: https://ci.appveyor.com/project/huge-success/sanic | ||||
| .. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest | ||||
|   | ||||
							
								
								
									
										1
									
								
								changelogs/1970.misc.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								changelogs/1970.misc.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| Adds py.typed file to expose type information to other packages. | ||||
| @@ -88,5 +88,5 @@ When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of ` | ||||
|  | ||||
|     app = Sanic(__name__) | ||||
|  | ||||
|     chunk_size = 1024 * 1024 * 8 # Set chunk size to 8KB | ||||
|     chunk_size = 1024 * 1024 * 8 # Set chunk size to 8MiB | ||||
|     app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size) | ||||
|   | ||||
| @@ -1,28 +1,83 @@ | ||||
| import os | ||||
| import sys | ||||
|  | ||||
| from argparse import ArgumentParser | ||||
| from argparse import ArgumentParser, RawDescriptionHelpFormatter | ||||
| from importlib import import_module | ||||
| from typing import Any, Dict, Optional | ||||
|  | ||||
| from sanic import __version__ | ||||
| from sanic.app import Sanic | ||||
| from sanic.config import BASE_LOGO | ||||
| from sanic.log import logger | ||||
|  | ||||
|  | ||||
| class SanicArgumentParser(ArgumentParser): | ||||
|     def add_bool_arguments(self, *args, **kwargs): | ||||
|         group = self.add_mutually_exclusive_group() | ||||
|         group.add_argument(*args, action="store_true", **kwargs) | ||||
|         kwargs["help"] = "no " + kwargs["help"] | ||||
|         group.add_argument( | ||||
|             "--no-" + args[0][2:], *args[1:], action="store_false", **kwargs | ||||
|         ) | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     parser = ArgumentParser(prog="sanic") | ||||
|     parser.add_argument("--host", dest="host", type=str, default="127.0.0.1") | ||||
|     parser.add_argument("--port", dest="port", type=int, default=8000) | ||||
|     parser.add_argument("--unix", dest="unix", type=str, default="") | ||||
|     parser = SanicArgumentParser( | ||||
|         prog="sanic", | ||||
|         description=BASE_LOGO, | ||||
|         formatter_class=RawDescriptionHelpFormatter, | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "-H", | ||||
|         "--host", | ||||
|         dest="host", | ||||
|         type=str, | ||||
|         default="127.0.0.1", | ||||
|         help="host address [default 127.0.0.1]", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "-p", | ||||
|         "--port", | ||||
|         dest="port", | ||||
|         type=int, | ||||
|         default=8000, | ||||
|         help="port to serve on [default 8000]", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "-u", | ||||
|         "--unix", | ||||
|         dest="unix", | ||||
|         type=str, | ||||
|         default="", | ||||
|         help="location of unix socket", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--cert", dest="cert", type=str, help="location of certificate for SSL" | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--key", dest="key", type=str, help="location of keyfile for SSL." | ||||
|     ) | ||||
|     parser.add_argument("--workers", dest="workers", type=int, default=1) | ||||
|     parser.add_argument( | ||||
|         "-w", | ||||
|         "--workers", | ||||
|         dest="workers", | ||||
|         type=int, | ||||
|         default=1, | ||||
|         help="number of worker processes [default 1]", | ||||
|     ) | ||||
|     parser.add_argument("--debug", dest="debug", action="store_true") | ||||
|     parser.add_argument("module") | ||||
|     parser.add_bool_arguments( | ||||
|         "--access-logs", dest="access_log", help="display access logs" | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "-v", | ||||
|         "--version", | ||||
|         action="version", | ||||
|         version=f"Sanic {__version__}", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "module", help="path to your Sanic app. Example: path.to.server:app" | ||||
|     ) | ||||
|     args = parser.parse_args() | ||||
|  | ||||
|     try: | ||||
| @@ -30,6 +85,9 @@ def main(): | ||||
|         if module_path not in sys.path: | ||||
|             sys.path.append(module_path) | ||||
|  | ||||
|         if ":" in args.module: | ||||
|             module_name, app_name = args.module.rsplit(":", 1) | ||||
|         else: | ||||
|             module_parts = args.module.split(".") | ||||
|             module_name = ".".join(module_parts[:-1]) | ||||
|             app_name = module_parts[-1] | ||||
| @@ -57,6 +115,7 @@ def main(): | ||||
|             unix=args.unix, | ||||
|             workers=args.workers, | ||||
|             debug=args.debug, | ||||
|             access_log=args.access_log, | ||||
|             ssl=ssl, | ||||
|         ) | ||||
|     except ImportError as e: | ||||
|   | ||||
							
								
								
									
										20
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -68,7 +68,7 @@ class Sanic: | ||||
|  | ||||
|         self.name = name | ||||
|         self.asgi = False | ||||
|         self.router = router or Router() | ||||
|         self.router = router or Router(self) | ||||
|         self.request_class = request_class | ||||
|         self.error_handler = error_handler or ErrorHandler() | ||||
|         self.config = Config(load_env=load_env) | ||||
| @@ -900,7 +900,9 @@ class Sanic: | ||||
|         name = None | ||||
|         try: | ||||
|             # Fetch handler from router | ||||
|             handler, args, kwargs, uri, name = self.router.get(request) | ||||
|             handler, args, kwargs, uri, name, endpoint = self.router.get( | ||||
|                 request | ||||
|             ) | ||||
|  | ||||
|             # -------------------------------------------- # | ||||
|             # Request Middleware | ||||
| @@ -922,16 +924,8 @@ class Sanic: | ||||
|                             "handler from the router" | ||||
|                         ) | ||||
|                     ) | ||||
|                 else: | ||||
|                     if not getattr(handler, "__blueprintname__", False): | ||||
|                         request.endpoint = self._build_endpoint_name( | ||||
|                             handler.__name__ | ||||
|                         ) | ||||
|                     else: | ||||
|                         request.endpoint = self._build_endpoint_name( | ||||
|                             getattr(handler, "__blueprintname__", ""), | ||||
|                             handler.__name__, | ||||
|                         ) | ||||
|  | ||||
|                 request.endpoint = endpoint | ||||
|  | ||||
|                 # Run response handler | ||||
|                 response = handler(request, *args, **kwargs) | ||||
| @@ -1454,6 +1448,8 @@ class Sanic: | ||||
|         asgi_app = await ASGIApp.create(self, scope, receive, send) | ||||
|         await asgi_app() | ||||
|  | ||||
|     _asgi_single_callable = True  # We conform to ASGI 3.0 single-callable | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
|     # Configuration | ||||
|     # -------------------------------------------------------------------- # | ||||
|   | ||||
| @@ -312,13 +312,19 @@ class ASGIApp: | ||||
|         callback = None if self.ws else self.stream_callback | ||||
|         await handler(self.request, None, callback) | ||||
|  | ||||
|     async def stream_callback(self, response: HTTPResponse) -> None: | ||||
|     _asgi_single_callable = True  # We conform to ASGI 3.0 single-callable | ||||
|  | ||||
|     async def stream_callback( | ||||
|         self, response: Union[HTTPResponse, StreamingHTTPResponse] | ||||
|     ) -> None: | ||||
|         """ | ||||
|         Write the response. | ||||
|         """ | ||||
|         headers: List[Tuple[bytes, bytes]] = [] | ||||
|         cookies: Dict[str, str] = {} | ||||
|         content_length: List[str] = [] | ||||
|         try: | ||||
|             content_length = response.headers.popall("content-length", []) | ||||
|             cookies = { | ||||
|                 v.key: v | ||||
|                 for _, v in list( | ||||
| @@ -351,12 +357,22 @@ class ASGIApp: | ||||
|             ] | ||||
|  | ||||
|         response.asgi = True | ||||
|  | ||||
|         if "content-length" not in response.headers and not isinstance( | ||||
|             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 += [ | ||||
|                 (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: | ||||
|   | ||||
| @@ -179,8 +179,8 @@ def abort(status_code, message=None): | ||||
|     message appropriate for the given status code, unless provided. | ||||
|  | ||||
|     :param status_code: The HTTP status code to return. | ||||
|     :param message: The HTTP response body. Defaults to the messages | ||||
|                     in response.py for the given status code. | ||||
|     :param message: The HTTP response body. Defaults to the messages in | ||||
|     STATUS_CODES from sanic.helpers for the given status code. | ||||
|     """ | ||||
|     if message is None: | ||||
|         message = STATUS_CODES.get(status_code) | ||||
|   | ||||
							
								
								
									
										0
									
								
								sanic/py.typed
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								sanic/py.typed
									
									
									
									
									
										Normal file
									
								
							| @@ -100,6 +100,8 @@ class StreamingHTTPResponse(BaseHTTPResponse): | ||||
|         """ | ||||
|         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: | ||||
|             await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data)) | ||||
|         else: | ||||
|   | ||||
| @@ -11,7 +11,16 @@ from sanic.views import CompositionView | ||||
|  | ||||
|  | ||||
| Route = namedtuple( | ||||
|     "Route", ["handler", "methods", "pattern", "parameters", "name", "uri"] | ||||
|     "Route", | ||||
|     [ | ||||
|         "handler", | ||||
|         "methods", | ||||
|         "pattern", | ||||
|         "parameters", | ||||
|         "name", | ||||
|         "uri", | ||||
|         "endpoint", | ||||
|     ], | ||||
| ) | ||||
| Parameter = namedtuple("Parameter", ["name", "cast"]) | ||||
|  | ||||
| @@ -79,7 +88,8 @@ class Router: | ||||
|     routes_always_check = None | ||||
|     parameter_pattern = re.compile(r"<(.+?)>") | ||||
|  | ||||
|     def __init__(self): | ||||
|     def __init__(self, app): | ||||
|         self.app = app | ||||
|         self.routes_all = {} | ||||
|         self.routes_names = {} | ||||
|         self.routes_static_files = {} | ||||
| @@ -299,11 +309,15 @@ class Router: | ||||
|  | ||||
|             handler_name = f"{bp_name}.{name or handler.__name__}" | ||||
|         else: | ||||
|             handler_name = name or getattr(handler, "__name__", None) | ||||
|             handler_name = name or getattr( | ||||
|                 handler, "__name__", handler.__class__.__name__ | ||||
|             ) | ||||
|  | ||||
|         if route: | ||||
|             route = merge_route(route, methods, handler) | ||||
|         else: | ||||
|             endpoint = self.app._build_endpoint_name(handler_name) | ||||
|  | ||||
|             route = Route( | ||||
|                 handler=handler, | ||||
|                 methods=methods, | ||||
| @@ -311,6 +325,7 @@ class Router: | ||||
|                 parameters=parameters, | ||||
|                 name=handler_name, | ||||
|                 uri=uri, | ||||
|                 endpoint=endpoint, | ||||
|             ) | ||||
|  | ||||
|         self.routes_all[uri] = route | ||||
| @@ -449,7 +464,8 @@ class Router: | ||||
|         route_handler = route.handler | ||||
|         if hasattr(route_handler, "handlers"): | ||||
|             route_handler = route_handler.handlers[method] | ||||
|         return route_handler, [], kwargs, route.uri, route.name | ||||
|  | ||||
|         return route_handler, [], kwargs, route.uri, route.name, route.endpoint | ||||
|  | ||||
|     def is_stream_handler(self, request): | ||||
|         """Handler for request is stream or not. | ||||
|   | ||||
| @@ -90,6 +90,7 @@ class CompositionView: | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.handlers = {} | ||||
|         self.name = self.__class__.__name__ | ||||
|  | ||||
|     def add(self, methods, handler, stream=False): | ||||
|         if stream: | ||||
|   | ||||
							
								
								
									
										8
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								setup.py
									
									
									
									
									
								
							| @@ -57,6 +57,7 @@ setup_kwargs = { | ||||
|     ), | ||||
|     "long_description": long_description, | ||||
|     "packages": ["sanic"], | ||||
|     "package_data": {"sanic": ["py.typed"]}, | ||||
|     "platforms": "any", | ||||
|     "python_requires": ">=3.6", | ||||
|     "classifiers": [ | ||||
| @@ -79,15 +80,15 @@ requirements = [ | ||||
|     "httptools>=0.0.10", | ||||
|     uvloop, | ||||
|     ujson, | ||||
|     "aiofiles>=0.3.0", | ||||
|     "aiofiles>=0.6.0", | ||||
|     "websockets>=8.1,<9.0", | ||||
|     "multidict==5.0.0", | ||||
|     "multidict>=5.0,<6.0", | ||||
|     "httpx==0.15.4", | ||||
| ] | ||||
|  | ||||
| tests_require = [ | ||||
|     "pytest==5.2.1", | ||||
|     "multidict==5.0.0", | ||||
|     "multidict>=5.0,<6.0", | ||||
|     "gunicorn", | ||||
|     "pytest-cov", | ||||
|     "httpcore==0.3.0", | ||||
| @@ -97,6 +98,7 @@ tests_require = [ | ||||
|     "pytest-sanic", | ||||
|     "pytest-sugar", | ||||
|     "pytest-benchmark", | ||||
|     "pytest-dependency", | ||||
| ] | ||||
|  | ||||
| docs_require = [ | ||||
|   | ||||
| @@ -95,10 +95,10 @@ class RouteStringGenerator: | ||||
|  | ||||
|  | ||||
| @pytest.fixture(scope="function") | ||||
| def sanic_router(): | ||||
| def sanic_router(app): | ||||
|     # noinspection PyProtectedMember | ||||
|     def _setup(route_details: tuple) -> (Router, tuple): | ||||
|         router = Router() | ||||
|         router = Router(app) | ||||
|         added_router = [] | ||||
|         for method, route in route_details: | ||||
|             try: | ||||
|   | ||||
| @@ -117,7 +117,7 @@ def test_app_route_raise_value_error(app): | ||||
|  | ||||
| def test_app_handle_request_handler_is_none(app, monkeypatch): | ||||
|     def mockreturn(*args, **kwargs): | ||||
|         return None, [], {}, "", "" | ||||
|         return None, [], {}, "", "", None | ||||
|  | ||||
|     # Not sure how to make app.router.get() return None, so use mock here. | ||||
|     monkeypatch.setattr(app.router, "get", mockreturn) | ||||
|   | ||||
| @@ -735,6 +735,7 @@ def test_static_blueprint_name(app: Sanic, static_file_directory, file_name): | ||||
|     _, response = app.test_client.get("/static/test.file/") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("file_name", ["test.file"]) | ||||
| def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name): | ||||
|     current_file = inspect.getfile(inspect.currentframe()) | ||||
| @@ -745,7 +746,7 @@ def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name): | ||||
|  | ||||
|     bp = Blueprint(name="test_mw", url_prefix="") | ||||
|  | ||||
|     @bp.middleware('request') | ||||
|     @bp.middleware("request") | ||||
|     def bp_mw1(request): | ||||
|         nonlocal triggered | ||||
|         triggered = True | ||||
| @@ -754,7 +755,7 @@ def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name): | ||||
|         "/test.file", | ||||
|         get_file_path(static_file_directory, file_name), | ||||
|         strict_slashes=True, | ||||
|         name="static" | ||||
|         name="static", | ||||
|     ) | ||||
|  | ||||
|     app.blueprint(bp) | ||||
|   | ||||
| @@ -20,7 +20,9 @@ def test_load_module_from_file_location(loaded_module_from_file_location): | ||||
|  | ||||
|  | ||||
| @pytest.mark.dependency(depends=["test_load_module_from_file_location"]) | ||||
| def test_loaded_module_from_file_location_name(loaded_module_from_file_location,): | ||||
| def test_loaded_module_from_file_location_name( | ||||
|     loaded_module_from_file_location, | ||||
| ): | ||||
|     name = loaded_module_from_file_location.__name__ | ||||
|     if "C:\\" in name: | ||||
|         name = name.split("\\")[-1] | ||||
|   | ||||
| @@ -238,7 +238,7 @@ def test_chunked_streaming_returns_correct_content(streaming_app): | ||||
| @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" | ||||
|     assert response.text == "foo,bar" | ||||
|  | ||||
|  | ||||
| def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Adam Hopkins
					Adam Hopkins