From aea2e4e7f4300421f87e98bffafb2534310d7d70 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Mon, 15 Feb 2021 14:18:42 +0200 Subject: [PATCH 1/5] Fix uvloop to 0.14.0 --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index c3f79166..6ec9ad5d 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,8 @@ setup_kwargs = { "author": "Sanic Community", "author_email": "admhpkns@gmail.com", "description": ( - "A web server and web framework that's written to go fast. Build fast. Run fast." + "A web server and web framework that's written to go fast. " + "Build fast. Run fast." ), "long_description": long_description, "packages": ["sanic"], @@ -80,7 +81,7 @@ env_dependency = ( '; sys_platform != "win32" ' 'and implementation_name == "cpython"' ) ujson = "ujson>=1.35" + env_dependency -uvloop = "uvloop>=0.5.3" + env_dependency +uvloop = "uvloop==0.14.0" + env_dependency requirements = [ "httptools>=0.0.10", From 7f63ad548458e289885feb4551f069913dae4a6f Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Mon, 15 Feb 2021 21:50:20 +0200 Subject: [PATCH 2/5] Add some test coverage --- sanic/app.py | 3 +- sanic/compat.py | 2 +- sanic/mixins/exceptions.py | 2 +- sanic/mixins/listeners.py | 2 +- sanic/mixins/middleware.py | 16 ++- sanic/mixins/routes.py | 205 +++++++++++++++++++++++++++++++++- sanic/static.py | 186 ------------------------------ tests/test_blueprint_group.py | 23 +++- tests/test_middleware.py | 65 +++++++++++ tests/test_routes.py | 13 +++ tests/test_server_events.py | 28 +++++ tests/test_static.py | 34 ++++++ 12 files changed, 378 insertions(+), 201 deletions(-) delete mode 100644 sanic/static.py diff --git a/sanic/app.py b/sanic/app.py index c676d952..2696dab0 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -49,7 +49,6 @@ from sanic.server import ( serve, serve_multiple, ) -from sanic.static import register as static_register from sanic.websocket import ConnectionClosed, WebSocketProtocol @@ -242,7 +241,7 @@ class Sanic(BaseSanic): return self.router.add(**params) def _apply_static(self, static: FutureStatic) -> Route: - return static_register(self, static) + return self._register_static(static) def _apply_middleware( self, diff --git a/sanic/compat.py b/sanic/compat.py index 393cad17..b925f7aa 100644 --- a/sanic/compat.py +++ b/sanic/compat.py @@ -17,7 +17,7 @@ class Header(CIMultiDict): use_trio = argv[0].endswith("hypercorn") and "trio" in argv -if use_trio: +if use_trio: # pragma: no cover import trio # type: ignore def stat_async(path): diff --git a/sanic/mixins/exceptions.py b/sanic/mixins/exceptions.py index c48988de..aed1ae5f 100644 --- a/sanic/mixins/exceptions.py +++ b/sanic/mixins/exceptions.py @@ -8,7 +8,7 @@ class ExceptionMixin: self._future_exceptions: Set[FutureException] = set() def _apply_exception_handler(self, handler: FutureException): - raise NotImplementedError + raise NotImplementedError # noqa def exception(self, *exceptions, apply=True): """ diff --git a/sanic/mixins/listeners.py b/sanic/mixins/listeners.py index 1688f8b7..e926208e 100644 --- a/sanic/mixins/listeners.py +++ b/sanic/mixins/listeners.py @@ -20,7 +20,7 @@ class ListenerMixin: self._future_listeners: List[FutureListener] = list() def _apply_listener(self, listener: FutureListener): - raise NotImplementedError + raise NotImplementedError # noqa def listener(self, listener_or_event, event_or_none=None, apply=True): """Create a listener from a decorated function. diff --git a/sanic/mixins/middleware.py b/sanic/mixins/middleware.py index 03db8752..a7e8f644 100644 --- a/sanic/mixins/middleware.py +++ b/sanic/mixins/middleware.py @@ -9,7 +9,7 @@ class MiddlewareMixin: self._future_middleware: List[FutureMiddleware] = list() def _apply_middleware(self, middleware: FutureMiddleware): - raise NotImplementedError + raise NotImplementedError # noqa def middleware( self, middleware_or_request, attach_to="request", apply=True @@ -42,8 +42,14 @@ class MiddlewareMixin: register_middleware, attach_to=middleware_or_request ) - def on_request(self, middleware): - return self.middleware(middleware, "request") + def on_request(self, middleware=None): + if callable(middleware): + return self.middleware(middleware, "request") + else: + return partial(self.middleware, attach_to="request") - def on_response(self, middleware): - return self.middleware(middleware, "response") + def on_response(self, middleware=None): + if callable(middleware): + return self.middleware(middleware, "response") + else: + return partial(self.middleware, attach_to="response") diff --git a/sanic/mixins/routes.py b/sanic/mixins/routes.py index 93ee1001..08044b49 100644 --- a/sanic/mixins/routes.py +++ b/sanic/mixins/routes.py @@ -1,11 +1,27 @@ +from functools import partial, wraps from inspect import signature +from mimetypes import guess_type +from os import path from pathlib import PurePath +from re import sub +from time import gmtime, strftime from typing import Set, Union +from urllib.parse import unquote from sanic_routing.route import Route # type: ignore +from sanic.compat import stat_async from sanic.constants import HTTP_METHODS +from sanic.exceptions import ( + ContentRangeError, + FileNotFound, + HeaderNotFound, + InvalidUsage, +) +from sanic.handlers import ContentRangeHandler +from sanic.log import error_logger from sanic.models.futures import FutureRoute, FutureStatic +from sanic.response import HTTPResponse, file, file_stream from sanic.views import CompositionView @@ -17,10 +33,10 @@ class RouteMixin: self.strict_slashes = False def _apply_route(self, route: FutureRoute) -> Route: - raise NotImplementedError + raise NotImplementedError # noqa def _apply_static(self, static: FutureStatic) -> Route: - raise NotImplementedError + raise NotImplementedError # noqa def route( self, @@ -555,10 +571,191 @@ class RouteMixin: else: break - if not name: - raise Exception("...") + if not name: # noq + raise ValueError("Could not generate a name for handler") if not name.startswith(f"{self.name}."): name = f"{self.name}.{name}" return name + + async def _static_request_handler( + self, + file_or_directory, + use_modified_since, + use_content_range, + stream_large_files, + request, + content_type=None, + file_uri=None, + ): + # Using this to determine if the URL is trying to break out of the path + # served. os.path.realpath seems to be very slow + if file_uri and "../" in file_uri: + raise InvalidUsage("Invalid URL") + # Merge served directory and requested file if provided + # Strip all / that in the beginning of the URL to help prevent python + # from herping a derp and treating the uri as an absolute path + root_path = file_path = file_or_directory + if file_uri: + file_path = path.join( + file_or_directory, sub("^[/]*", "", file_uri) + ) + + # URL decode the path sent by the browser otherwise we won't be able to + # match filenames which got encoded (filenames with spaces etc) + file_path = path.abspath(unquote(file_path)) + if not file_path.startswith(path.abspath(unquote(root_path))): + error_logger.exception( + f"File not found: path={file_or_directory}, " + f"relative_url={file_uri}" + ) + raise FileNotFound( + "File not found", path=file_or_directory, relative_url=file_uri + ) + try: + headers = {} + # Check if the client has been sent this file before + # and it has not been modified since + stats = None + if use_modified_since: + stats = await stat_async(file_path) + modified_since = strftime( + "%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime) + ) + if request.headers.get("If-Modified-Since") == modified_since: + return HTTPResponse(status=304) + headers["Last-Modified"] = modified_since + _range = None + if use_content_range: + _range = None + if not stats: + stats = await stat_async(file_path) + headers["Accept-Ranges"] = "bytes" + headers["Content-Length"] = str(stats.st_size) + if request.method != "HEAD": + try: + _range = ContentRangeHandler(request, stats) + except HeaderNotFound: + pass + else: + del headers["Content-Length"] + for key, value in _range.headers.items(): + headers[key] = value + + if "content-type" not in headers: + content_type = ( + content_type + or guess_type(file_path)[0] + or "application/octet-stream" + ) + + if "charset=" not in content_type and ( + content_type.startswith("text/") + or content_type == "application/javascript" + ): + content_type += "; charset=utf-8" + + headers["Content-Type"] = content_type + + if request.method == "HEAD": + return HTTPResponse(headers=headers) + else: + if stream_large_files: + if type(stream_large_files) == int: + threshold = stream_large_files + else: + threshold = 1024 * 1024 + + if not stats: + stats = await stat_async(file_path) + if stats.st_size >= threshold: + return await file_stream( + file_path, headers=headers, _range=_range + ) + return await file(file_path, headers=headers, _range=_range) + except ContentRangeError: + raise + except Exception: + error_logger.exception( + f"File not found: path={file_or_directory}, " + f"relative_url={file_uri}" + ) + raise FileNotFound( + "File not found", path=file_or_directory, relative_url=file_uri + ) + + def _register_static( + self, + static: FutureStatic, + ): + # TODO: Though sanic is not a file server, I feel like we should + # at least make a good effort here. Modified-since is nice, but + # we could also look into etags, expires, and caching + """ + Register a static directory handler with Sanic by adding a route to the + router and registering a handler. + + :param app: Sanic + :param file_or_directory: File or directory path to serve from + :type file_or_directory: Union[str,bytes,Path] + :param uri: URL to serve from + :type uri: str + :param pattern: regular expression used to match files in the URL + :param use_modified_since: If true, send file modified time, and return + not modified if the browser's matches the + server's + :param use_content_range: If true, process header for range requests + and sends the file part that is requested + :param stream_large_files: If true, use the file_stream() handler + rather than the file() handler to send the file + If this is an integer, this represents the + threshold size to switch to file_stream() + :param name: user defined name used for url_for + :type name: str + :param content_type: user defined content type for header + :return: registered static routes + :rtype: List[sanic.router.Route] + """ + + if isinstance(static.file_or_directory, bytes): + file_or_directory = static.file_or_directory.decode("utf-8") + elif isinstance(static.file_or_directory, PurePath): + file_or_directory = str(static.file_or_directory) + elif not isinstance(static.file_or_directory, str): + raise ValueError("Invalid file path string.") + else: + file_or_directory = static.file_or_directory + + uri = static.uri + name = static.name + # If we're not trying to match a file directly, + # serve from the folder + if not path.isfile(file_or_directory): + uri += "/" + + # special prefix for static files + # if not static.name.startswith("_static_"): + # name = f"_static_{static.name}" + + _handler = wraps(self._static_request_handler)( + partial( + self._static_request_handler, + file_or_directory, + static.use_modified_since, + static.use_content_range, + static.stream_large_files, + content_type=static.content_type, + ) + ) + + route, _ = self.route( + uri=uri, + methods=["GET", "HEAD"], + name=name, + host=static.host, + strict_slashes=static.strict_slashes, + static=True, + )(_handler) + + return route diff --git a/sanic/static.py b/sanic/static.py deleted file mode 100644 index 45cbc214..00000000 --- a/sanic/static.py +++ /dev/null @@ -1,186 +0,0 @@ -from functools import partial, wraps -from mimetypes import guess_type -from os import path -from pathlib import PurePath -from re import sub -from time import gmtime, strftime -from urllib.parse import unquote - -from sanic.compat import stat_async -from sanic.exceptions import ( - ContentRangeError, - FileNotFound, - HeaderNotFound, - InvalidUsage, -) -from sanic.handlers import ContentRangeHandler -from sanic.log import error_logger -from sanic.models.futures import FutureStatic -from sanic.response import HTTPResponse, file, file_stream - - -async def _static_request_handler( - file_or_directory, - use_modified_since, - use_content_range, - stream_large_files, - request, - content_type=None, - file_uri=None, -): - # Using this to determine if the URL is trying to break out of the path - # served. os.path.realpath seems to be very slow - if file_uri and "../" in file_uri: - raise InvalidUsage("Invalid URL") - # Merge served directory and requested file if provided - # Strip all / that in the beginning of the URL to help prevent python - # from herping a derp and treating the uri as an absolute path - root_path = file_path = file_or_directory - if file_uri: - file_path = path.join(file_or_directory, sub("^[/]*", "", file_uri)) - - # URL decode the path sent by the browser otherwise we won't be able to - # match filenames which got encoded (filenames with spaces etc) - file_path = path.abspath(unquote(file_path)) - if not file_path.startswith(path.abspath(unquote(root_path))): - error_logger.exception( - f"File not found: path={file_or_directory}, " - f"relative_url={file_uri}" - ) - raise FileNotFound( - "File not found", path=file_or_directory, relative_url=file_uri - ) - try: - headers = {} - # Check if the client has been sent this file before - # and it has not been modified since - stats = None - if use_modified_since: - stats = await stat_async(file_path) - modified_since = strftime( - "%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime) - ) - if request.headers.get("If-Modified-Since") == modified_since: - return HTTPResponse(status=304) - headers["Last-Modified"] = modified_since - _range = None - if use_content_range: - _range = None - if not stats: - stats = await stat_async(file_path) - headers["Accept-Ranges"] = "bytes" - headers["Content-Length"] = str(stats.st_size) - if request.method != "HEAD": - try: - _range = ContentRangeHandler(request, stats) - except HeaderNotFound: - pass - else: - del headers["Content-Length"] - for key, value in _range.headers.items(): - headers[key] = value - headers["Content-Type"] = ( - content_type or guess_type(file_path)[0] or "text/plain" - ) - if request.method == "HEAD": - return HTTPResponse(headers=headers) - else: - if stream_large_files: - if type(stream_large_files) == int: - threshold = stream_large_files - else: - threshold = 1024 * 1024 - - if not stats: - stats = await stat_async(file_path) - if stats.st_size >= threshold: - return await file_stream( - file_path, headers=headers, _range=_range - ) - return await file(file_path, headers=headers, _range=_range) - except ContentRangeError: - raise - except Exception: - error_logger.exception( - f"File not found: path={file_or_directory}, " - f"relative_url={file_uri}" - ) - raise FileNotFound( - "File not found", path=file_or_directory, relative_url=file_uri - ) - - -def register( - app, - static: FutureStatic, -): - # TODO: Though sanic is not a file server, I feel like we should at least - # make a good effort here. Modified-since is nice, but we could - # also look into etags, expires, and caching - """ - Register a static directory handler with Sanic by adding a route to the - router and registering a handler. - - :param app: Sanic - :param file_or_directory: File or directory path to serve from - :type file_or_directory: Union[str,bytes,Path] - :param uri: URL to serve from - :type uri: str - :param pattern: regular expression used to match files in the URL - :param use_modified_since: If true, send file modified time, and return - not modified if the browser's matches the - server's - :param use_content_range: If true, process header for range requests - and sends the file part that is requested - :param stream_large_files: If true, use the file_stream() handler rather - than the file() handler to send the file - If this is an integer, this represents the - threshold size to switch to file_stream() - :param name: user defined name used for url_for - :type name: str - :param content_type: user defined content type for header - :return: registered static routes - :rtype: List[sanic.router.Route] - """ - - if isinstance(static.file_or_directory, bytes): - file_or_directory = static.file_or_directory.decode("utf-8") - elif isinstance(static.file_or_directory, PurePath): - file_or_directory = str(static.file_or_directory) - elif not isinstance(static.file_or_directory, str): - raise ValueError("Invalid file path string.") - else: - file_or_directory = static.file_or_directory - - uri = static.uri - name = static.name - # If we're not trying to match a file directly, - # serve from the folder - if not path.isfile(file_or_directory): - uri += "/" - - # special prefix for static files - # if not static.name.startswith("_static_"): - # name = f"_static_{static.name}" - - _handler = wraps(_static_request_handler)( - partial( - _static_request_handler, - file_or_directory, - static.use_modified_since, - static.use_content_range, - static.stream_large_files, - content_type=static.content_type, - ) - ) - - route, _ = app.route( - uri=uri, - methods=["GET", "HEAD"], - name=name, - host=static.host, - strict_slashes=static.strict_slashes, - static=True, - )(_handler) - - return route diff --git a/tests/test_blueprint_group.py b/tests/test_blueprint_group.py index 813ec2ff..df2bda88 100644 --- a/tests/test_blueprint_group.py +++ b/tests/test_blueprint_group.py @@ -110,6 +110,11 @@ def test_bp_group(app: Sanic): global MIDDLEWARE_INVOKE_COUNTER MIDDLEWARE_INVOKE_COUNTER["request"] += 1 + @blueprint_group_1.middleware + def blueprint_group_1_middleware_not_called(request): + global MIDDLEWARE_INVOKE_COUNTER + MIDDLEWARE_INVOKE_COUNTER["request"] += 1 + @blueprint_3.route("/") def blueprint_3_default_route(request): return text("BP3_OK") @@ -142,7 +147,7 @@ def test_bp_group(app: Sanic): assert response.text == "BP3_OK" assert MIDDLEWARE_INVOKE_COUNTER["response"] == 3 - assert MIDDLEWARE_INVOKE_COUNTER["request"] == 2 + assert MIDDLEWARE_INVOKE_COUNTER["request"] == 4 def test_bp_group_list_operations(app: Sanic): @@ -179,3 +184,19 @@ def test_bp_group_list_operations(app: Sanic): assert len(blueprint_group_1) == 2 assert blueprint_group_1.url_prefix == "/bp" + + +def test_bp_group_as_list(): + blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1") + blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2") + blueprint_group_1 = Blueprint.group([blueprint_1, blueprint_2]) + assert len(blueprint_group_1) == 2 + + +def test_bp_group_as_nested_group(): + blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1") + blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2") + blueprint_group_1 = Blueprint.group( + Blueprint.group(blueprint_1, blueprint_2) + ) + assert len(blueprint_group_1) == 2 diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 69883c3e..0d0ca5ec 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -30,6 +30,23 @@ def test_middleware_request(app): assert type(results[0]) is Request +def test_middleware_request_as_convenience(app): + results = [] + + @app.on_request + async def handler1(request): + results.append(request) + + @app.route("/") + async def handler2(request): + return text("OK") + + request, response = app.test_client.get("/") + + assert response.text == "OK" + assert type(results[0]) is Request + + def test_middleware_response(app): results = [] @@ -54,6 +71,54 @@ def test_middleware_response(app): assert isinstance(results[2], HTTPResponse) +def test_middleware_response_as_convenience(app): + results = [] + + @app.on_request + async def process_request(request): + results.append(request) + + @app.on_response + async def process_response(request, response): + results.append(request) + results.append(response) + + @app.route("/") + async def handler(request): + return text("OK") + + request, response = app.test_client.get("/") + + assert response.text == "OK" + assert type(results[0]) is Request + assert type(results[1]) is Request + assert isinstance(results[2], HTTPResponse) + + +def test_middleware_response_as_convenience_called(app): + results = [] + + @app.on_request() + async def process_request(request): + results.append(request) + + @app.on_response() + async def process_response(request, response): + results.append(request) + results.append(response) + + @app.route("/") + async def handler(request): + return text("OK") + + request, response = app.test_client.get("/") + + assert response.text == "OK" + assert type(results[0]) is Request + assert type(results[1]) is Request + assert isinstance(results[2], HTTPResponse) + + def test_middleware_response_exception(app): result = {"status_code": "middleware not run"} diff --git a/tests/test_routes.py b/tests/test_routes.py index e0591913..f3477400 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -633,6 +633,19 @@ def test_websocket_route(app, url): assert ev.is_set() +def test_websocket_route_invalid_handler(app): + with pytest.raises(ValueError) as e: + + @app.websocket("/") + async def handler(): + ... + + assert e.match( + r"Required parameter `request` and/or `ws` missing in the " + r"handler\(\) route\?" + ) + + @pytest.mark.asyncio @pytest.mark.parametrize("url", ["/ws", "ws"]) async def test_websocket_route_asgi(app, url): diff --git a/tests/test_server_events.py b/tests/test_server_events.py index 4b41f6fa..2e48f408 100644 --- a/tests/test_server_events.py +++ b/tests/test_server_events.py @@ -8,6 +8,8 @@ import pytest from sanic_testing.testing import HOST, PORT +from sanic.exceptions import InvalidUsage + AVAILABLE_LISTENERS = [ "before_server_start", @@ -80,6 +82,18 @@ def test_all_listeners(app): assert app.name + listener_name == output.pop() +@skipif_no_alarm +def test_all_listeners_as_convenience(app): + output = [] + for listener_name in AVAILABLE_LISTENERS: + listener = create_listener(listener_name, output) + method = getattr(app, listener_name) + method(listener) + start_stop_app(app) + for listener_name in AVAILABLE_LISTENERS: + assert app.name + listener_name == output.pop() + + @pytest.mark.asyncio async def test_trigger_before_events_create_server(app): class MySanicDb: @@ -95,6 +109,20 @@ async def test_trigger_before_events_create_server(app): assert isinstance(app.db, MySanicDb) +@pytest.mark.asyncio +async def test_trigger_before_events_create_server_missing_event(app): + class MySanicDb: + pass + + with pytest.raises(InvalidUsage): + + @app.listener + async def init_db(app, loop): + app.db = MySanicDb() + + assert not hasattr(app, "db") + + def test_create_server_trigger_events(app): """Test if create_server can trigger server events""" diff --git a/tests/test_static.py b/tests/test_static.py index d116c7b9..c67ff439 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -127,6 +127,40 @@ def test_static_file_content_type(app, static_file_directory, file_name): assert response.headers["Content-Type"] == "text/html; charset=utf-8" +@pytest.mark.parametrize( + "file_name,expected", + [ + ("test.html", "text/html; charset=utf-8"), + ("decode me.txt", "text/plain; charset=utf-8"), + ("test.file", "application/octet-stream"), + ], +) +def test_static_file_content_type_guessed( + app, static_file_directory, file_name, expected +): + app.static( + "/testing.file", + get_file_path(static_file_directory, file_name), + ) + + request, response = app.test_client.get("/testing.file") + assert response.status == 200 + assert response.body == get_file_content(static_file_directory, file_name) + assert response.headers["Content-Type"] == expected + + +def test_static_file_content_type_with_charset(app, static_file_directory): + app.static( + "/testing.file", + get_file_path(static_file_directory, "decode me.txt"), + content_type="text/plain;charset=ISO-8859-1", + ) + + request, response = app.test_client.get("/testing.file") + assert response.status == 200 + assert response.headers["Content-Type"] == "text/plain;charset=ISO-8859-1" + + @pytest.mark.parametrize( "file_name", ["test.file", "decode me.txt", "symlink", "hard_link"] ) From a913e712a76af67ea8ac0e7aa4bfbcdd540b9a6b Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Mon, 15 Feb 2021 22:45:21 +0200 Subject: [PATCH 3/5] test coverage --- sanic/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/utils.py b/sanic/utils.py index dce61619..519d3c29 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -44,7 +44,7 @@ def str_to_bool(val: str) -> bool: def load_module_from_file_location( location: Union[bytes, str, Path], encoding: str = "utf8", *args, **kwargs -): +): # noqa """Returns loaded module provided as a file path. :param args: From c942a5c51c4f057222c3de83e9ffc6c6a1324e04 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Tue, 16 Feb 2021 01:17:34 +0200 Subject: [PATCH 4/5] Change uvloop ro range --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6ec9ad5d..db669b17 100644 --- a/setup.py +++ b/setup.py @@ -81,7 +81,7 @@ env_dependency = ( '; sys_platform != "win32" ' 'and implementation_name == "cpython"' ) ujson = "ujson>=1.35" + env_dependency -uvloop = "uvloop==0.14.0" + env_dependency +uvloop = "uvloop>=0.5.3,<0.15.0" + env_dependency requirements = [ "httptools>=0.0.10", From 15502182e32e916524407b9865e3f61668237751 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Tue, 16 Feb 2021 09:07:12 +0200 Subject: [PATCH 5/5] Update changelog --- CHANGELOG.rst | 304 ++++++++++++++++++++++++++++---------------------- 1 file changed, 171 insertions(+), 133 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cb828d00..f63084aa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,31 @@ +Version 20.12.2 +=============== + +Dependencies +************ + + * + `#2026 `_ + Fix uvloop to 0.14 because 0.15 drops Python 3.6 support + + * + `#2029 `_ + Remove old chardet requirement, add in hard multidict requirement + +Version 19.12.5 +=============== + +Dependencies +************ + + * + `#2025 `_ + Fix uvloop to 0.14 because 0.15 drops Python 3.6 support + + * + `#2027 `_ + Remove old chardet requirement, add in hard multidict requirement + Version 20.12.0 =============== @@ -5,83 +33,93 @@ Features ******** * - `#1945 `_ + `#1993 `_ + Add disable app registry + +Version 20.12.0 +=============== + +Features +******** + + * + `#1945 `_ Static route more verbose if file not found * - `#1954 `_ + `#1954 `_ Fix static routes registration on a blueprint * - `#1961 `_ + `#1961 `_ Add Python 3.9 support * - `#1962 `_ + `#1962 `_ Sanic CLI upgrade * - `#1967 `_ + `#1967 `_ Update aiofile version requirements * - `#1969 `_ + `#1969 `_ Update multidict version requirements * - `#1970 `_ + `#1970 `_ Add py.typed file * - `#1972 `_ + `#1972 `_ Speed optimization in request handler * - `#1979 `_ + `#1979 `_ Add app registry and Sanic class level app retrieval Bugfixes ******** * - `#1965 `_ + `#1965 `_ Fix Chunked Transport-Encoding in ASGI streaming response Deprecations and Removals ************************* * - `#1981 `_ + `#1981 `_ Cleanup and remove deprecated code Developer infrastructure ************************ * - `#1956 `_ + `#1956 `_ Fix load module test * - `#1973 `_ + `#1973 `_ Transition Travis from .org to .com * - `#1986 `_ + `#1986 `_ Update tox requirements Improved Documentation ********************** * - `#1951 `_ + `#1951 `_ Documentation improvements * - `#1983 `_ + `#1983 `_ Remove duplicate contents in testing.rst * - `#1984 `_ + `#1984 `_ Fix typo in routing.rst @@ -92,10 +130,10 @@ Bugfixes ******** * - `#1954 `_ + `#1954 `_ Fix static route registration on blueprints * - `#1957 `_ + `#1957 `_ Removes duplicate headers in ASGI streaming body @@ -106,7 +144,7 @@ Bugfixes ******** * - `#1959 `_ + `#1959 `_ Removes duplicate headers in ASGI streaming body @@ -118,65 +156,65 @@ Features ******** * - `#1887 `_ + `#1887 `_ Pass subprotocols in websockets (both sanic server and ASGI) * - `#1894 `_ + `#1894 `_ Automatically set ``test_mode`` flag on app instance * - `#1903 `_ + `#1903 `_ Add new unified method for updating app values * - `#1906 `_, - `#1909 `_ + `#1906 `_, + `#1909 `_ Adds WEBSOCKET_PING_TIMEOUT and WEBSOCKET_PING_INTERVAL configuration values * - `#1935 `_ + `#1935 `_ httpx version dependency updated, it is slated for removal as a dependency in v20.12 * - `#1937 `_ + `#1937 `_ Added auto, text, and json fallback error handlers (in v21.3, the default will change form html to auto) Bugfixes ******** * - `#1897 `_ + `#1897 `_ Resolves exception from unread bytes in stream Deprecations and Removals ************************* * - `#1903 `_ + `#1903 `_ config.from_envar, config.from_pyfile, and config.from_object are deprecated and set to be removed in v21.3 Developer infrastructure ************************ * - `#1890 `_, - `#1891 `_ + `#1890 `_, + `#1891 `_ Update isort calls to be compatible with new API * - `#1893 `_ + `#1893 `_ Remove version section from setup.cfg * - `#1924 `_ + `#1924 `_ Adding --strict-markers for pytest Improved Documentation ********************** * - `#1922 `_ + `#1922 `_ Add explicit ASGI compliance to the README @@ -187,7 +225,7 @@ Bugfixes ******** * - `#1884 `_ + `#1884 `_ Revert change to multiprocessing mode @@ -198,7 +236,7 @@ Features ******** * - `#1641 `_ + `#1641 `_ Socket binding implemented properly for IPv6 and UNIX sockets @@ -209,60 +247,60 @@ Features ******** * - `#1760 `_ + `#1760 `_ Add version parameter to websocket routes * - `#1866 `_ + `#1866 `_ Add ``sanic`` as an entry point command * - `#1880 `_ + `#1880 `_ Add handler names for websockets for url_for usage Bugfixes ******** * - `#1776 `_ + `#1776 `_ Bug fix for host parameter issue with lists * - `#1842 `_ + `#1842 `_ Fix static _handler pickling error * - `#1827 `_ + `#1827 `_ Fix reloader on OSX py38 and Windows * - `#1848 `_ + `#1848 `_ Reverse named_response_middlware execution order, to match normal response middleware execution order * - `#1853 `_ + `#1853 `_ Fix pickle error when attempting to pickle an application which contains websocket routes Deprecations and Removals ************************* * - `#1739 `_ + `#1739 `_ Deprecate body_bytes to merge into body Developer infrastructure ************************ * - `#1852 `_ + `#1852 `_ Fix naming of CI test env on Python nightlies * - `#1857 `_ + `#1857 `_ Adjust websockets version to setup.py * - `#1869 `_ + `#1869 `_ Wrap run()'s "protocol" type annotation in Optional[] @@ -270,11 +308,11 @@ Improved Documentation ********************** * - `#1846 `_ + `#1846 `_ Update docs to clarify response middleware execution order * - `#1865 `_ + `#1865 `_ Fixing rst format issue that was hiding documentation @@ -291,127 +329,127 @@ Features ******** * - `#1762 `_ + `#1762 `_ Add ``srv.start_serving()`` and ``srv.serve_forever()`` to ``AsyncioServer`` * - `#1767 `_ + `#1767 `_ Make Sanic usable on ``hypercorn -k trio myweb.app`` * - `#1768 `_ + `#1768 `_ No tracebacks on normal errors and prettier error pages * - `#1769 `_ + `#1769 `_ Code cleanup in file responses * - `#1793 `_ and - `#1819 `_ + `#1793 `_ and + `#1819 `_ Upgrade ``str.format()`` to f-strings * - `#1798 `_ + `#1798 `_ Allow multiple workers on MacOS with Python 3.8 * - `#1820 `_ + `#1820 `_ Do not set content-type and content-length headers in exceptions Bugfixes ******** * - `#1748 `_ + `#1748 `_ Remove loop argument in ``asyncio.Event`` in Python 3.8 * - `#1764 `_ + `#1764 `_ Allow route decorators to stack up again * - `#1789 `_ + `#1789 `_ Fix tests using hosts yielding incorrect ``url_for`` * - `#1808 `_ + `#1808 `_ Fix Ctrl+C and tests on Windows Deprecations and Removals ************************* * - `#1800 `_ + `#1800 `_ Begin deprecation in way of first-class streaming, removal of ``body_init``, ``body_push``, and ``body_finish`` * - `#1801 `_ - Complete deprecation from `#1666 `_ of dictionary context on ``request`` objects. + `#1801 `_ + Complete deprecation from `#1666 `_ of dictionary context on ``request`` objects. * - `#1807 `_ + `#1807 `_ Remove server config args that can be read directly from app * - `#1818 `_ + `#1818 `_ Complete deprecation of ``app.remove_route`` and ``request.raw_args`` Dependencies ************ * - `#1794 `_ + `#1794 `_ Bump ``httpx`` to 0.11.1 * - `#1806 `_ + `#1806 `_ Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation) Developer infrastructure ************************ * - `#1833 `_ + `#1833 `_ Resolve broken documentation builds Improved Documentation ********************** * - `#1755 `_ + `#1755 `_ Usage of ``response.empty()`` * - `#1778 `_ + `#1778 `_ Update README * - `#1783 `_ + `#1783 `_ Fix typo * - `#1784 `_ - Corrected changelog for docs move of MD to RST (`#1691 `_) + `#1784 `_ + Corrected changelog for docs move of MD to RST (`#1691 `_) * - `#1803 `_ + `#1803 `_ Update config docs to match DEFAULT_CONFIG * - `#1814 `_ + `#1814 `_ Update getting_started.rst * - `#1821 `_ + `#1821 `_ Update to deployment * - `#1822 `_ + `#1822 `_ Update docs with changes done in 20.3 * - `#1834 `_ + `#1834 `_ Order of listeners @@ -431,11 +469,11 @@ Bugfixes - If you register a middleware via :code:`@blueprint.middleware` then it will apply only to the routes defined by the blueprint. - If you register a middleware via :code:`@blueprint_group.middleware` then it will apply to all blueprint based routes that are part of the group. - - If you define a middleware via :code:`@app.middleware` then it will be applied on all available routes (`#37 `__) + - If you define a middleware via :code:`@app.middleware` then it will be applied on all available routes (`#37 `__) - Fix `url_for` behavior with missing SERVER_NAME If the `SERVER_NAME` was missing in the `app.config` entity, the `url_for` on the `request` and `app` were failing - due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 `__) + due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 `__) Improved Documentation @@ -444,10 +482,10 @@ Improved Documentation - Move docs from MD to RST Moved all docs from markdown to restructured text like the rest of the docs to unify the scheme and make it easier in - the future to update documentation. (`#1691 `__) + the future to update documentation. (`#1691 `__) - Fix documentation for `get` and `getlist` of the `request.args` - Add additional example for showing the usage of `getlist` and fix the documentation string for `request.args` behavior (`#1704 `__) + Add additional example for showing the usage of `getlist` and fix the documentation string for `request.args` behavior (`#1704 `__) Version 19.6.3 @@ -459,7 +497,7 @@ Features - Enable Towncrier Support As part of this feature, `towncrier` is being introduced as a mechanism to partially automate the process - of generating and managing change logs as part of each of pull requests. (`#1631 `__) + of generating and managing change logs as part of each of pull requests. (`#1631 `__) Improved Documentation @@ -470,7 +508,7 @@ Improved Documentation - Enable having a single common `CHANGELOG` file for both GitHub page and documentation - Fix Sphinix deprecation warnings - Fix documentation warnings due to invalid `rst` indentation - - Enable common contribution guidelines file across GitHub and documentation via `CONTRIBUTING.rst` (`#1631 `__) + - Enable common contribution guidelines file across GitHub and documentation via `CONTRIBUTING.rst` (`#1631 `__) Version 19.6.2 @@ -480,16 +518,16 @@ Features ******** * - `#1562 `_ + `#1562 `_ Remove ``aiohttp`` dependency and create new ``SanicTestClient`` based upon `requests-async `_ * - `#1475 `_ + `#1475 `_ Added ASGI support (Beta) * - `#1436 `_ + `#1436 `_ Add Configure support from object string @@ -497,34 +535,34 @@ Bugfixes ******** * - `#1587 `_ + `#1587 `_ Add missing handle for Expect header. * - `#1560 `_ + `#1560 `_ Allow to disable Transfer-Encoding: chunked. * - `#1558 `_ + `#1558 `_ Fix graceful shutdown. * - `#1594 `_ + `#1594 `_ Strict Slashes behavior fix Deprecations and Removals ************************* * - `#1544 `_ + `#1544 `_ Drop dependency on distutil * - `#1562 `_ + `#1562 `_ Drop support for Python 3.5 * - `#1568 `_ + `#1568 `_ Deprecate route removal. .. warning:: @@ -541,39 +579,39 @@ Features ******** * - `#1497 `_ + `#1497 `_ Add support for zero-length and RFC 5987 encoded filename for multipart/form-data requests. * - `#1484 `_ + `#1484 `_ The type of ``expires`` attribute of ``sanic.cookies.Cookie`` is now enforced to be of type ``datetime``. * - `#1482 `_ + `#1482 `_ Add support for the ``stream`` parameter of ``sanic.Sanic.add_route()`` available to ``sanic.Blueprint.add_route()``. * - `#1481 `_ + `#1481 `_ Accept negative values for route parameters with type ``int`` or ``number``. * - `#1476 `_ + `#1476 `_ Deprecated the use of ``sanic.request.Request.raw_args`` - it has a fundamental flaw in which is drops repeated query string parameters. Added ``sanic.request.Request.query_args`` as a replacement for the original use-case. * - `#1472 `_ + `#1472 `_ Remove an unwanted ``None`` check in Request class ``repr`` implementation. This changes the default ``repr`` of a Request from ```` to ```` * - `#1470 `_ + `#1470 `_ Added 2 new parameters to ``sanic.app.Sanic.create_server``\ : @@ -584,21 +622,21 @@ Features This is a breaking change. * - `#1499 `_ + `#1499 `_ Added a set of test cases that test and benchmark route resolution. * - `#1457 `_ + `#1457 `_ The type of the ``"max-age"`` value in a ``sanic.cookies.Cookie`` is now enforced to be an integer. Non-integer values are replaced with ``0``. * - `#1445 `_ + `#1445 `_ Added the ``endpoint`` attribute to an incoming ``request``\ , containing the name of the handler function. * - `#1423 `_ + `#1423 `_ Improved request streaming. ``request.stream`` is now a bounded-size buffer instead of an unbounded queue. Callers must now call ``await request.stream.read()`` instead of ``await request.stream.get()`` @@ -611,33 +649,33 @@ Bugfixes * - `#1502 `_ + `#1502 `_ Sanic was prefetching ``time.time()`` and updating it once per second to avoid excessive ``time.time()`` calls. The implementation was observed to cause memory leaks in some cases. The benefit of the prefetch appeared to negligible, so this has been removed. Fixes - `#1500 `_ + `#1500 `_ * - `#1501 `_ + `#1501 `_ Fix a bug in the auto-reloader when the process was launched as a module i.e. ``python -m init0.mod1`` where the sanic server is started in ``init0/mod1.py`` with ``debug`` enabled and imports another module in ``init0``. * - `#1376 `_ + `#1376 `_ Allow sanic test client to bind to a random port by specifying ``port=None`` when constructing a ``SanicTestClient`` * - `#1399 `_ + `#1399 `_ Added the ability to specify middleware on a blueprint group, so that all routes produced from the blueprints in the group have the middleware applied. * - `#1442 `_ + `#1442 `_ Allow the the use the ``SANIC_ACCESS_LOG`` environment variable to enable/disable the access log when not explicitly passed to ``app.run()``. This allows the access log to be disabled for example when running via @@ -646,29 +684,29 @@ Bugfixes Developer infrastructure ************************ - * `#1529 `_ Update project PyPI credentials - * `#1515 `_ fix linter issue causing travis build failures (fix #1514) - * `#1490 `_ Fix python version in doc build - * `#1478 `_ Upgrade setuptools version and use native docutils in doc build - * `#1464 `_ Upgrade pytest, and fix caplog unit tests + * `#1529 `_ Update project PyPI credentials + * `#1515 `_ fix linter issue causing travis build failures (fix #1514) + * `#1490 `_ Fix python version in doc build + * `#1478 `_ Upgrade setuptools version and use native docutils in doc build + * `#1464 `_ Upgrade pytest, and fix caplog unit tests Improved Documentation ********************** - * `#1516 `_ Fix typo at the exception documentation - * `#1510 `_ fix typo in Asyncio example - * `#1486 `_ Documentation typo - * `#1477 `_ Fix grammar in README.md - * `#1489 `_ Added "databases" to the extensions list - * `#1483 `_ Add sanic-zipkin to extensions list - * `#1487 `_ Removed link to deleted repo, Sanic-OAuth, from the extensions list - * `#1460 `_ 18.12 changelog - * `#1449 `_ Add example of amending request object - * `#1446 `_ Update README - * `#1444 `_ Update README - * `#1443 `_ Update README, including new logo - * `#1440 `_ fix minor type and pip install instruction mismatch - * `#1424 `_ Documentation Enhancements + * `#1516 `_ Fix typo at the exception documentation + * `#1510 `_ fix typo in Asyncio example + * `#1486 `_ Documentation typo + * `#1477 `_ Fix grammar in README.md + * `#1489 `_ Added "databases" to the extensions list + * `#1483 `_ Add sanic-zipkin to extensions list + * `#1487 `_ Removed link to deleted repo, Sanic-OAuth, from the extensions list + * `#1460 `_ 18.12 changelog + * `#1449 `_ Add example of amending request object + * `#1446 `_ Update README + * `#1444 `_ Update README + * `#1443 `_ Update README, including new logo + * `#1440 `_ fix minor type and pip install instruction mismatch + * `#1424 `_ Documentation Enhancements Note: 19.3.0 was skipped for packagement purposes and not released on PyPI @@ -725,7 +763,7 @@ Version 0.8 * Changes: - * Ownership changed to org 'huge-success' + * Ownership changed to org 'sanic-org' 0.8.0 *****