From 028778ed56201905ba0cf2121c03819267c17e5c Mon Sep 17 00:00:00 2001 From: 7 Date: Mon, 16 Dec 2019 07:46:18 -0800 Subject: [PATCH 01/18] Fix #1714 (#1716) * fix abort call errors out when calling inside stream handler * handle pending task properly after cleanup --- sanic/server.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/sanic/server.py b/sanic/server.py index 41af81c0..bde6def7 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -364,6 +364,21 @@ class HttpProtocol(asyncio.Protocol): else: self.request.body_push(body) + async def body_append(self, body): + if ( + self.request is None + or self._request_stream_task is None + or self._request_stream_task.cancelled() + ): + return + + if self.request.stream.is_full(): + self.transport.pause_reading() + await self.request.stream.put(body) + self.transport.resume_reading() + else: + await self.request.stream.put(body) + async def stream_append(self): while self._body_chunks: body = self._body_chunks.popleft() From c3aed010960d2e93d9d688be29af502bb9a201f2 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 18 Dec 2019 16:02:33 -0800 Subject: [PATCH 02/18] testing: Add host argument to SanicTestClient Adds the ability to specify a host argument when using the SanicTestClient. Signed-off-by: Eli Uriegas --- sanic/testing.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/sanic/testing.py b/sanic/testing.py index f4925b7f..c052967f 100644 --- a/sanic/testing.py +++ b/sanic/testing.py @@ -22,10 +22,11 @@ PORT = 42101 class SanicTestClient: - def __init__(self, app, port=PORT): + def __init__(self, app, port=PORT, host=HOST): """Use port=None to bind to a random port""" self.app = app self.port = port + self.host = host def get_new_session(self): return requests.Session() @@ -71,6 +72,7 @@ class SanicTestClient: gather_request=True, debug=False, server_kwargs={"auto_reload": False}, + host=None, *request_args, **request_kwargs, ): @@ -95,11 +97,13 @@ class SanicTestClient: return self.app.error_handler.default(request, exception) if self.port: - server_kwargs = dict(host=HOST, port=self.port, **server_kwargs) - host, port = HOST, self.port + server_kwargs = dict( + host=host or self.host, port=self.port, **server_kwargs + ) + host, port = host or self.host, self.port else: sock = socket() - sock.bind((HOST, 0)) + sock.bind((host or self.host, 0)) server_kwargs = dict(sock=sock, **server_kwargs) host, port = sock.getsockname() From a6077a179067812766293f9ab4391b5e1499a9c6 Mon Sep 17 00:00:00 2001 From: Harsha Narayana Date: Fri, 20 Dec 2019 21:31:05 +0530 Subject: [PATCH 03/18] GIT-37: fix blueprint middleware application (#1690) * GIT-37: fix blueprint middleware application 1. If you register a middleware via `@blueprint.middleware` then it will apply only to the routes defined by the blueprint. 2. If you register a middleware via `@blueprint_group.middleware` then it will apply to all blueprint based routes that are part of the group. 3. If you define a middleware via `@app.middleware` then it will be applied on all available routes Fixes #37 Signed-off-by: Harsha Narayana * GIT-37: add changelog Signed-off-by: Harsha Narayana --- changelogs/37.bugfix.rst | 11 ++++++ sanic/app.py | 67 +++++++++++++++++++++++++---------- sanic/blueprints.py | 20 ++++++++--- sanic/router.py | 17 +++++---- tests/test_app.py | 2 +- tests/test_blueprint_group.py | 6 ++-- tests/test_blueprints.py | 2 +- 7 files changed, 91 insertions(+), 34 deletions(-) create mode 100644 changelogs/37.bugfix.rst diff --git a/changelogs/37.bugfix.rst b/changelogs/37.bugfix.rst new file mode 100644 index 00000000..80c00a3f --- /dev/null +++ b/changelogs/37.bugfix.rst @@ -0,0 +1,11 @@ +Fix blueprint middleware application + +Currently, any blueprint middleware registered, irrespective of which blueprint was used to do so, was +being applied to all of the routes created by the :code:`@app` and :code:`@blueprint` alike. + +As part of this change, the blueprint based middleware application is enforced based on where they are +registered. + +- 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 diff --git a/sanic/app.py b/sanic/app.py index 58c785b0..65e84809 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -85,7 +85,8 @@ class Sanic: self.is_request_stream = False self.websocket_enabled = False self.websocket_tasks = set() - + self.named_request_middleware = {} + self.named_response_middleware = {} # Register alternative method names self.go_fast = self.run @@ -178,7 +179,7 @@ class Sanic: :param stream: :param version: :param name: user defined route name for url_for - :return: decorated function + :return: tuple of routes, decorated function """ # Fix case where the user did not prefix the URL with a / @@ -204,7 +205,7 @@ class Sanic: if stream: handler.is_stream = stream - self.router.add( + routes = self.router.add( uri=uri, methods=methods, handler=handler, @@ -213,7 +214,7 @@ class Sanic: version=version, name=name, ) - return handler + return routes, handler return response @@ -462,7 +463,7 @@ class Sanic: :param subprotocols: optional list of str with supported subprotocols :param name: A unique name assigned to the URL so that it can be used with :func:`url_for` - :return: decorated function + :return: tuple of routes, decorated function """ self.enable_websocket() @@ -515,7 +516,7 @@ class Sanic: self.websocket_tasks.remove(fut) await ws.close() - self.router.add( + routes = self.router.add( uri=uri, handler=websocket_handler, methods=frozenset({"GET"}), @@ -523,7 +524,7 @@ class Sanic: strict_slashes=strict_slashes, name=name, ) - return handler + return routes, handler return response @@ -544,6 +545,7 @@ class Sanic: :param host: Host IP or FQDN details :param uri: URL path that will be mapped to the websocket handler + handler :param strict_slashes: If the API endpoint needs to terminate with a "/" or not :param subprotocols: Subprotocols to be used with websocket @@ -645,6 +647,22 @@ class Sanic: self.response_middleware.appendleft(middleware) return middleware + def register_named_middleware( + self, middleware, route_names, attach_to="request" + ): + if attach_to == "request": + for _rn in route_names: + if _rn not in self.named_request_middleware: + self.named_request_middleware[_rn] = deque() + if middleware not in self.named_request_middleware[_rn]: + self.named_request_middleware[_rn].append(middleware) + if attach_to == "response": + for _rn in route_names: + if _rn not in self.named_response_middleware: + self.named_response_middleware[_rn] = deque() + if middleware not in self.named_response_middleware[_rn]: + self.named_response_middleware[_rn].append(middleware) + # Decorator def middleware(self, middleware_or_request): """ @@ -916,20 +934,23 @@ class Sanic: # allocation before assignment below. response = None cancelled = False + name = None try: + # Fetch handler from router + handler, args, kwargs, uri, name = self.router.get(request) + # -------------------------------------------- # # Request Middleware # -------------------------------------------- # - response = await self._run_request_middleware(request) + response = await self._run_request_middleware( + request, request_name=name + ) # No middleware results if not response: # -------------------------------------------- # # Execute Handler # -------------------------------------------- # - # Fetch handler from router - handler, args, kwargs, uri = self.router.get(request) - request.uri_template = uri if handler is None: raise ServerError( @@ -993,7 +1014,7 @@ class Sanic: if response is not None: try: response = await self._run_response_middleware( - request, response + request, response, request_name=name ) except CancelledError: # Response middleware can timeout too, as above. @@ -1265,10 +1286,14 @@ class Sanic: if isawaitable(result): await result - async def _run_request_middleware(self, request): + async def _run_request_middleware(self, request, request_name=None): # The if improves speed. I don't know why - if self.request_middleware: - for middleware in self.request_middleware: + named_middleware = self.named_request_middleware.get( + request_name, deque() + ) + applicable_middleware = self.request_middleware + named_middleware + if applicable_middleware: + for middleware in applicable_middleware: response = middleware(request) if isawaitable(response): response = await response @@ -1276,9 +1301,15 @@ class Sanic: return response return None - async def _run_response_middleware(self, request, response): - if self.response_middleware: - for middleware in self.response_middleware: + async def _run_response_middleware( + self, request, response, request_name=None + ): + named_middleware = self.named_response_middleware.get( + request_name, deque() + ) + applicable_middleware = self.response_middleware + named_middleware + if applicable_middleware: + for middleware in applicable_middleware: _response = middleware(request, response) if isawaitable(_response): _response = await _response diff --git a/sanic/blueprints.py b/sanic/blueprints.py index b4894f62..511e5162 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -104,6 +104,8 @@ class Blueprint: url_prefix = options.get("url_prefix", self.url_prefix) + routes = [] + # Routes for future in self.routes: # attach the blueprint name to the handler so that it can be @@ -114,7 +116,7 @@ class Blueprint: version = future.version or self.version - app.route( + _routes, _ = app.route( uri=uri[1:] if uri.startswith("//") else uri, methods=future.methods, host=future.host or self.host, @@ -123,6 +125,8 @@ class Blueprint: version=version, name=future.name, )(future.handler) + if _routes: + routes += _routes for future in self.websocket_routes: # attach the blueprint name to the handler so that it can be @@ -130,21 +134,27 @@ class Blueprint: future.handler.__blueprintname__ = self.name # Prepend the blueprint URI prefix if available uri = url_prefix + future.uri if url_prefix else future.uri - app.websocket( + _routes, _ = app.websocket( uri=uri, host=future.host or self.host, strict_slashes=future.strict_slashes, name=future.name, )(future.handler) + if _routes: + routes += _routes + route_names = [route.name for route in routes] # Middleware for future in self.middlewares: if future.args or future.kwargs: - app.register_middleware( - future.middleware, *future.args, **future.kwargs + app.register_named_middleware( + future.middleware, + route_names, + *future.args, + **future.kwargs ) else: - app.register_middleware(future.middleware) + app.register_named_middleware(future.middleware, route_names) # Exceptions for future in self.exceptions: diff --git a/sanic/router.py b/sanic/router.py index 63119446..2d8817a3 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -140,21 +140,22 @@ class Router: docs for further details. :return: Nothing """ + routes = [] if version is not None: version = re.escape(str(version).strip("/").lstrip("v")) uri = "/".join(["/v{}".format(version), uri.lstrip("/")]) # add regular version - self._add(uri, methods, handler, host, name) + routes.append(self._add(uri, methods, handler, host, name)) if strict_slashes: - return + return routes if not isinstance(host, str) and host is not None: # we have gotten back to the top of the recursion tree where the # host was originally a list. By now, we've processed the strict # slashes logic on the leaf nodes (the individual host strings in # the list of host) - return + return routes # Add versions with and without trailing / slashed_methods = self.routes_all.get(uri + "/", frozenset({})) @@ -176,10 +177,12 @@ class Router: ) # add version with trailing slash if slash_is_missing: - self._add(uri + "/", methods, handler, host, name) + routes.append(self._add(uri + "/", methods, handler, host, name)) # add version without trailing slash elif without_slash_is_missing: - self._add(uri[:-1], methods, handler, host, name) + routes.append(self._add(uri[:-1], methods, handler, host, name)) + + return routes def _add(self, uri, methods, handler, host=None, name=None): """Add a handler to the route list @@ -328,6 +331,7 @@ class Router: self.routes_dynamic[url_hash(uri)].append(route) else: self.routes_static[uri] = route + return route @staticmethod def check_dynamic_route_exists(pattern, routes_to_check, parameters): @@ -442,6 +446,7 @@ class Router: method=method, allowed_methods=self.get_supported_methods(url), ) + if route: if route.methods and method not in route.methods: raise method_not_supported @@ -476,7 +481,7 @@ class Router: route_handler = route.handler if hasattr(route_handler, "handlers"): route_handler = route_handler.handlers[method] - return route_handler, [], kwargs, route.uri + return route_handler, [], kwargs, route.uri, route.name def is_stream_handler(self, request): """ Handler for request is stream or not. diff --git a/tests/test_app.py b/tests/test_app.py index deb050b6..77171094 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -94,7 +94,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, [], {}, "", "" # Not sure how to make app.router.get() return None, so use mock here. monkeypatch.setattr(app.router, "get", mockreturn) diff --git a/tests/test_blueprint_group.py b/tests/test_blueprint_group.py index 32729a49..5a4b805c 100644 --- a/tests/test_blueprint_group.py +++ b/tests/test_blueprint_group.py @@ -83,7 +83,7 @@ def test_bp_group_with_additional_route_params(app: Sanic): _, response = app.test_client.patch("/api/bp2/route/bp2", headers=header) assert response.text == "PATCH_bp2" - _, response = app.test_client.get("/v2/api/bp1/request_path") + _, response = app.test_client.put("/v2/api/bp1/request_path") assert response.status == 401 @@ -141,8 +141,8 @@ def test_bp_group(app: Sanic): _, response = app.test_client.get("/api/bp3") assert response.text == "BP3_OK" - assert MIDDLEWARE_INVOKE_COUNTER["response"] == 4 - assert MIDDLEWARE_INVOKE_COUNTER["request"] == 4 + assert MIDDLEWARE_INVOKE_COUNTER["response"] == 3 + assert MIDDLEWARE_INVOKE_COUNTER["request"] == 2 def test_bp_group_list_operations(app: Sanic): diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 16a97309..c21f3807 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -268,7 +268,7 @@ def test_bp_middleware(app): request, response = app.test_client.get("/") assert response.status == 200 - assert response.text == "OK" + assert response.text == "FAIL" def test_bp_exception_handler(app): From 3f6a978328a1976a1c3d75cb8bda039ce3d1c11c Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Sat, 21 Dec 2019 05:23:52 +0200 Subject: [PATCH 04/18] Swap out requests-async for httpx (#1728) * Begin swap of requests-async for httpx * Finalize httpx adoption and resolve tests Resolve linting and formatting * Remove documentation references to requests-async in favor of httpx --- docs/sanic/testing.rst | 8 +- environment.yml | 2 +- sanic/asgi.py | 13 +- sanic/testing.py | 274 +++++------------------- setup.py | 3 +- tests/test_asgi.py | 27 ++- tests/test_keep_alive_timeout.py | 53 ++--- tests/test_request_buffer_queue_size.py | 1 + tests/test_request_stream.py | 2 +- tests/test_request_timeout.py | 81 ++++--- tests/test_requests.py | 34 +-- tests/test_utf8.py | 4 +- tox.ini | 2 +- 13 files changed, 171 insertions(+), 333 deletions(-) diff --git a/docs/sanic/testing.rst b/docs/sanic/testing.rst index 5dba2ab4..67506edc 100644 --- a/docs/sanic/testing.rst +++ b/docs/sanic/testing.rst @@ -2,7 +2,7 @@ Testing ======= Sanic endpoints can be tested locally using the `test_client` object, which -depends on the additional `requests-async `_ +depends on an additional package: `httpx `_ library, which implements an API that mirrors the `requests` library. The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods @@ -22,7 +22,7 @@ for you to run against your application. A simple example (using pytest) is like assert response.status == 405 Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and -your test request is executed against your application, using `requests-async`. +your test request is executed against your application, using `httpx`. The `test_client` methods accept the following arguments and keyword arguments: @@ -55,8 +55,8 @@ And to supply data to a JSON POST request: assert request.json.get('key1') == 'value1' More information about -the available arguments to `requests-async` can be found -[in the documentation for `requests `_. +the available arguments to `httpx` can be found +[in the documentation for `httpx `_. Using a random port diff --git a/environment.yml b/environment.yml index e42a97ef..dc88e646 100644 --- a/environment.yml +++ b/environment.yml @@ -13,7 +13,7 @@ dependencies: - sphinx==1.8.3 - sphinx_rtd_theme==0.4.2 - recommonmark==0.5.0 - - requests-async==0.5.0 + - httpx==0.9.3 - sphinxcontrib-asyncio>=0.2.0 - docutils==0.14 - pygments==2.3.1 diff --git a/sanic/asgi.py b/sanic/asgi.py index f9e49005..f08cc454 100644 --- a/sanic/asgi.py +++ b/sanic/asgi.py @@ -15,8 +15,6 @@ from typing import ( ) from urllib.parse import quote -from requests_async import ASGISession # type: ignore - import sanic.app # noqa from sanic.compat import Header @@ -189,7 +187,7 @@ class Lifespan: class ASGIApp: - sanic_app: Union[ASGISession, "sanic.app.Sanic"] + sanic_app: "sanic.app.Sanic" request: Request transport: MockTransport do_stream: bool @@ -223,8 +221,13 @@ class ASGIApp: if scope["type"] == "lifespan": await instance.lifespan(scope, receive, send) else: - url_bytes = scope.get("root_path", "") + quote(scope["path"]) - url_bytes = url_bytes.encode("latin-1") + path = ( + scope["path"][1:] + if scope["path"].startswith("/") + else scope["path"] + ) + url = "/".join([scope.get("root_path", ""), quote(path)]) + url_bytes = url.encode("latin-1") url_bytes += b"?" + scope["query_string"] if scope["type"] == "http": diff --git a/sanic/testing.py b/sanic/testing.py index c052967f..c836a943 100644 --- a/sanic/testing.py +++ b/sanic/testing.py @@ -1,14 +1,8 @@ -import asyncio -import types -import typing - from json import JSONDecodeError from socket import socket -from urllib.parse import unquote, urlsplit -import httpcore # type: ignore -import requests_async as requests # type: ignore -import websockets # type: ignore +import httpx +import websockets from sanic.asgi import ASGIApp from sanic.exceptions import MethodNotSupported @@ -29,7 +23,7 @@ class SanicTestClient: self.host = host def get_new_session(self): - return requests.Session() + return httpx.Client() async def _local_request(self, method, url, *args, **kwargs): logger.info(url) @@ -60,7 +54,8 @@ class SanicTestClient: if raw_cookies: response.raw_cookies = {} - for cookie in response.cookies: + + for cookie in response.cookies.jar: response.raw_cookies[cookie.name] = cookie return response @@ -179,181 +174,6 @@ class SanicTestClient: return self._sanic_endpoint_test("websocket", *args, **kwargs) -class SanicASGIAdapter(requests.asgi.ASGIAdapter): # noqa - async def send( # type: ignore - self, - request: requests.PreparedRequest, - gather_return: bool = False, - *args: typing.Any, - **kwargs: typing.Any, - ) -> requests.Response: - """This method is taken MOSTLY verbatim from requests-asyn. The - difference is the capturing of a response on the ASGI call and then - returning it on the response object. This is implemented to achieve: - - request, response = await app.asgi_client.get("/") - - You can see the original code here: - https://github.com/encode/requests-async/blob/614f40f77f19e6c6da8a212ae799107b0384dbf9/requests_async/asgi.py#L51""" # noqa - scheme, netloc, path, query, fragment = urlsplit( - request.url - ) # type: ignore - - default_port = {"http": 80, "ws": 80, "https": 443, "wss": 443}[scheme] - - if ":" in netloc: - host, port_string = netloc.split(":", 1) - port = int(port_string) - else: - host = netloc - port = default_port - - # Include the 'host' header. - if "host" in request.headers: - headers = [] # type: typing.List[typing.Tuple[bytes, bytes]] - elif port == default_port: - headers = [(b"host", host.encode())] - else: - headers = [(b"host", (f"{host}:{port}").encode())] - - # Include other request headers. - headers += [ - (key.lower().encode(), value.encode()) - for key, value in request.headers.items() - ] - - no_response = False - if scheme in {"ws", "wss"}: - subprotocol = request.headers.get("sec-websocket-protocol", None) - if subprotocol is None: - subprotocols = [] # type: typing.Sequence[str] - else: - subprotocols = [ - value.strip() for value in subprotocol.split(",") - ] - - scope = { - "type": "websocket", - "path": unquote(path), - "root_path": "", - "scheme": scheme, - "query_string": query.encode(), - "headers": headers, - "client": ["testclient", 50000], - "server": [host, port], - "subprotocols": subprotocols, - } - no_response = True - - else: - scope = { - "type": "http", - "http_version": "1.1", - "method": request.method, - "path": unquote(path), - "root_path": "", - "scheme": scheme, - "query_string": query.encode(), - "headers": headers, - "client": ["testclient", 50000], - "server": [host, port], - "extensions": {"http.response.template": {}}, - } - - async def receive(): - nonlocal request_complete, response_complete - - if request_complete: - while not response_complete: - await asyncio.sleep(0.0001) - return {"type": "http.disconnect"} - - body = request.body - if isinstance(body, str): - body_bytes = body.encode("utf-8") # type: bytes - elif body is None: - body_bytes = b"" - elif isinstance(body, types.GeneratorType): - try: - chunk = body.send(None) - if isinstance(chunk, str): - chunk = chunk.encode("utf-8") - return { - "type": "http.request", - "body": chunk, - "more_body": True, - } - except StopIteration: - request_complete = True - return {"type": "http.request", "body": b""} - else: - body_bytes = body - - request_complete = True - return {"type": "http.request", "body": body_bytes} - - request_complete = False - response_started = False - response_complete = False - raw_kwargs = {"content": b""} # type: typing.Dict[str, typing.Any] - template = None - context = None - return_value = None - - async def send(message) -> None: - nonlocal raw_kwargs, response_started, response_complete, template, context # noqa - - if message["type"] == "http.response.start": - assert ( - not response_started - ), 'Received multiple "http.response.start" messages.' - raw_kwargs["status_code"] = message["status"] - raw_kwargs["headers"] = message["headers"] - response_started = True - elif message["type"] == "http.response.body": - assert response_started, ( - 'Received "http.response.body" ' - 'without "http.response.start".' - ) - assert ( - not response_complete - ), 'Received "http.response.body" after response completed.' - body = message.get("body", b"") - more_body = message.get("more_body", False) - if request.method != "HEAD": - raw_kwargs["content"] += body - if not more_body: - response_complete = True - elif message["type"] == "http.response.template": - template = message["template"] - context = message["context"] - - try: - return_value = await self.app(scope, receive, send) - except BaseException as exc: - if not self.suppress_exceptions: - raise exc from None - - if no_response: - response_started = True - raw_kwargs = {"status_code": 204, "headers": []} - - if not self.suppress_exceptions: - assert response_started, "TestClient did not receive any response." - elif not response_started: - raw_kwargs = {"status_code": 500, "headers": []} - - raw = httpcore.Response(**raw_kwargs) - response = self.build_response(request, raw) - if template is not None: - response.template = template - response.context = context - - if gather_return: - response.return_value = return_value - return response - - class TestASGIApp(ASGIApp): async def __call__(self): await super().__call__() @@ -365,7 +185,11 @@ async def app_call_with_return(self, scope, receive, send): return await asgi_app() -class SanicASGITestClient(requests.ASGISession): +class SanicASGIDispatch(httpx.dispatch.ASGIDispatch): + pass + + +class SanicASGITestClient(httpx.Client): def __init__( self, app, @@ -374,18 +198,18 @@ class SanicASGITestClient(requests.ASGISession): ) -> None: app.__class__.__call__ = app_call_with_return app.asgi = True - super().__init__(app) - adapter = SanicASGIAdapter( - app, suppress_exceptions=suppress_exceptions - ) - self.mount("http://", adapter) - self.mount("https://", adapter) - self.mount("ws://", adapter) - self.mount("wss://", adapter) - self.headers.update({"user-agent": "testclient"}) self.app = app - self.base_url = base_url + + dispatch = SanicASGIDispatch(app=app, client=(ASGI_HOST, PORT)) + super().__init__(dispatch=dispatch, base_url=base_url) + + self.last_request = None + + def _collect_request(request): + self.last_request = request + + app.request_middleware.appendleft(_collect_request) async def request(self, method, url, gather_request=True, *args, **kwargs): @@ -395,33 +219,39 @@ class SanicASGITestClient(requests.ASGISession): response.body = response.content response.content_type = response.headers.get("content-type") - if hasattr(response, "return_value"): - request = response.return_value - del response.return_value - return request, response - - return response - - def merge_environment_settings(self, *args, **kwargs): - settings = super().merge_environment_settings(*args, **kwargs) - settings.update({"gather_return": self.gather_request}) - return settings + return self.last_request, response async def websocket(self, uri, subprotocols=None, *args, **kwargs): - if uri.startswith(("ws:", "wss:")): - url = uri - else: - uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri) - url = "ws://testserver{uri}".format(uri=uri) + scheme = "ws" + path = uri + root_path = "{}://{}".format(scheme, ASGI_HOST) - headers = kwargs.get("headers", {}) - headers.setdefault("connection", "upgrade") - headers.setdefault("sec-websocket-key", "testserver==") - headers.setdefault("sec-websocket-version", "13") - if subprotocols is not None: - headers.setdefault( - "sec-websocket-protocol", ", ".join(subprotocols) - ) - kwargs["headers"] = headers + headers = kwargs.get("headers", {}) + headers.setdefault("connection", "upgrade") + headers.setdefault("sec-websocket-key", "testserver==") + headers.setdefault("sec-websocket-version", "13") + if subprotocols is not None: + headers.setdefault( + "sec-websocket-protocol", ", ".join(subprotocols) + ) - return await self.request("websocket", url, **kwargs) + scope = { + "type": "websocket", + "asgi": {"version": "3.0"}, + "http_version": "1.1", + "headers": [map(lambda y: y.encode(), x) for x in headers.items()], + "scheme": scheme, + "root_path": root_path, + "path": path, + "query_string": b"", + } + + async def receive(): + return {} + + async def send(message): + pass + + await self.app(scope, receive, send) + + return None, {} diff --git a/setup.py b/setup.py index bd48d994..019769cf 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ import codecs import os import re import sys + from distutils.util import strtobool from setuptools import setup @@ -83,7 +84,7 @@ requirements = [ "aiofiles>=0.3.0", "websockets>=7.0,<9.0", "multidict>=4.0,<5.0", - "requests-async==0.5.0", + "httpx==0.9.3", ] tests_require = [ diff --git a/tests/test_asgi.py b/tests/test_asgi.py index bf024252..4b9ed58b 100644 --- a/tests/test_asgi.py +++ b/tests/test_asgi.py @@ -1,6 +1,6 @@ import asyncio -from collections import deque +from collections import deque, namedtuple import pytest import uvicorn @@ -245,17 +245,26 @@ async def test_cookie_customization(app): return response _, response = await app.asgi_client.get("/cookie") + + CookieDef = namedtuple("CookieDef", ("value", "httponly")) + Cookie = namedtuple("Cookie", ("domain", "path", "value", "httponly")) cookie_map = { - "test": {"value": "Cookie1", "HttpOnly": True}, - "c2": {"value": "Cookie2", "HttpOnly": False}, + "test": CookieDef("Cookie1", True), + "c2": CookieDef("Cookie2", False), } - for k, v in ( - response.cookies._cookies.get("mockserver.local").get("/").items() - ): - assert cookie_map.get(k).get("value") == v.value - if cookie_map.get(k).get("HttpOnly"): - assert "HttpOnly" in v._rest.keys() + cookies = { + c.name: Cookie(c.domain, c.path, c.value, "HttpOnly" in c._rest.keys()) + for c in response.cookies.jar + } + + for name, definition in cookie_map.items(): + cookie = cookies.get(name) + assert cookie + assert cookie.value == definition.value + assert cookie.domain == "mockserver.local" + assert cookie.path == "/" + assert cookie.httponly == definition.httponly @pytest.mark.asyncio diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index 672d78ac..a59d6c5b 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -1,15 +1,9 @@ import asyncio -import functools -import socket from asyncio import sleep as aio_sleep -from http.client import _encode from json import JSONDecodeError -import httpcore -import requests_async as requests - -from httpcore import PoolTimeout +import httpx from sanic import Sanic, server from sanic.response import text @@ -21,24 +15,28 @@ CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} old_conn = None -class ReusableSanicConnectionPool(httpcore.ConnectionPool): - async def acquire_connection(self, origin): +class ReusableSanicConnectionPool( + httpx.dispatch.connection_pool.ConnectionPool +): + async def acquire_connection(self, origin, timeout): global old_conn - connection = self.active_connections.pop_by_origin( - origin, http2_only=True - ) - if connection is None: - connection = self.keepalive_connections.pop_by_origin(origin) + connection = self.pop_connection(origin) if connection is None: - await self.max_connections.acquire() - connection = httpcore.HTTPConnection( + pool_timeout = None if timeout is None else timeout.pool_timeout + + await self.max_connections.acquire(timeout=pool_timeout) + connection = httpx.dispatch.connection.HTTPConnection( origin, - ssl=self.ssl, - timeout=self.timeout, + verify=self.verify, + cert=self.cert, + http2=self.http2, backend=self.backend, release_func=self.release_connection, + trust_env=self.trust_env, + uds=self.uds, ) + self.active_connections.add(connection) if old_conn is not None: @@ -51,17 +49,10 @@ class ReusableSanicConnectionPool(httpcore.ConnectionPool): return connection -class ReusableSanicAdapter(requests.adapters.HTTPAdapter): - def __init__(self): - self.pool = ReusableSanicConnectionPool() - - -class ResusableSanicSession(requests.Session): +class ResusableSanicSession(httpx.Client): def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - adapter = ReusableSanicAdapter() - self.mount("http://", adapter) - self.mount("https://", adapter) + dispatch = ReusableSanicConnectionPool() + super().__init__(dispatch=dispatch, *args, **kwargs) class ReuseableSanicTestClient(SanicTestClient): @@ -74,6 +65,9 @@ class ReuseableSanicTestClient(SanicTestClient): self._tcp_connector = None self._session = None + def get_new_session(self): + return ResusableSanicSession() + # Copied from SanicTestClient, but with some changes to reuse the # same loop for the same app. def _sanic_endpoint_test( @@ -167,7 +161,6 @@ class ReuseableSanicTestClient(SanicTestClient): self._server.close() self._loop.run_until_complete(self._server.wait_closed()) self._server = None - self.app.stop() if self._session: self._loop.run_until_complete(self._session.close()) @@ -186,7 +179,7 @@ class ReuseableSanicTestClient(SanicTestClient): "request_keepalive", CONFIG_FOR_TESTS["KEEP_ALIVE_TIMEOUT"] ) if not self._session: - self._session = ResusableSanicSession() + self._session = self.get_new_session() try: response = await getattr(self._session, method.lower())( url, verify=False, timeout=request_keepalive, *args, **kwargs diff --git a/tests/test_request_buffer_queue_size.py b/tests/test_request_buffer_queue_size.py index 1a9cfdf3..8e42c79a 100644 --- a/tests/test_request_buffer_queue_size.py +++ b/tests/test_request_buffer_queue_size.py @@ -2,6 +2,7 @@ import io from sanic.response import text + data = "abc" * 10_000_000 diff --git a/tests/test_request_stream.py b/tests/test_request_stream.py index 8f893e2b..404299c0 100644 --- a/tests/test_request_stream.py +++ b/tests/test_request_stream.py @@ -332,7 +332,7 @@ def test_request_stream_handle_exception(app): assert response.text == "Error: Requested URL /in_valid_post not found" # 405 - request, response = app.test_client.get("/post/random_id", data=data) + request, response = app.test_client.get("/post/random_id") assert response.status == 405 assert ( response.text == "Error: Method GET not allowed for URL" diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index e3e02d7c..4025c71b 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -1,49 +1,70 @@ import asyncio -import httpcore -import requests_async as requests +import httpx from sanic import Sanic from sanic.response import text from sanic.testing import SanicTestClient -class DelayableSanicConnectionPool(httpcore.ConnectionPool): +class DelayableHTTPConnection(httpx.dispatch.connection.HTTPConnection): + def __init__(self, *args, **kwargs): + self._request_delay = None + if "request_delay" in kwargs: + self._request_delay = kwargs.pop("request_delay") + super().__init__(*args, **kwargs) + + async def send(self, request, verify=None, cert=None, timeout=None): + if self.h11_connection is None and self.h2_connection is None: + await self.connect(verify=verify, cert=cert, timeout=timeout) + + if self._request_delay: + await asyncio.sleep(self._request_delay) + + if self.h2_connection is not None: + response = await self.h2_connection.send(request, timeout=timeout) + else: + assert self.h11_connection is not None + response = await self.h11_connection.send(request, timeout=timeout) + + return response + + +class DelayableSanicConnectionPool( + httpx.dispatch.connection_pool.ConnectionPool +): def __init__(self, request_delay=None, *args, **kwargs): self._request_delay = request_delay super().__init__(*args, **kwargs) - async def send(self, request, stream=False, ssl=None, timeout=None): - connection = await self.acquire_connection(request.url.origin) - if ( - connection.h11_connection is None - and connection.h2_connection is None - ): - await connection.connect(ssl=ssl, timeout=timeout) - if self._request_delay: - await asyncio.sleep(self._request_delay) - try: - response = await connection.send( - request, stream=stream, ssl=ssl, timeout=timeout + async def acquire_connection(self, origin, timeout=None): + connection = self.pop_connection(origin) + + if connection is None: + pool_timeout = None if timeout is None else timeout.pool_timeout + + await self.max_connections.acquire(timeout=pool_timeout) + connection = DelayableHTTPConnection( + origin, + verify=self.verify, + cert=self.cert, + http2=self.http2, + backend=self.backend, + release_func=self.release_connection, + trust_env=self.trust_env, + uds=self.uds, + request_delay=self._request_delay, ) - except BaseException as exc: - self.active_connections.remove(connection) - self.max_connections.release() - raise exc - return response + + self.active_connections.add(connection) + + return connection -class DelayableSanicAdapter(requests.adapters.HTTPAdapter): - def __init__(self, request_delay=None): - self.pool = DelayableSanicConnectionPool(request_delay=request_delay) - - -class DelayableSanicSession(requests.Session): +class DelayableSanicSession(httpx.Client): def __init__(self, request_delay=None, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - adapter = DelayableSanicAdapter(request_delay=request_delay) - self.mount("http://", adapter) - self.mount("https://", adapter) + dispatch = DelayableSanicConnectionPool(request_delay=request_delay) + super().__init__(dispatch=dispatch, *args, **kwargs) class DelayableSanicTestClient(SanicTestClient): diff --git a/tests/test_requests.py b/tests/test_requests.py index 2f83513c..93516e00 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -10,7 +10,7 @@ import pytest from sanic import Blueprint, Sanic from sanic.exceptions import ServerError -from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, RequestParameters +from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, Request, RequestParameters from sanic.response import json, text from sanic.testing import ASGI_HOST, HOST, PORT @@ -55,11 +55,11 @@ def test_ip(app): async def test_ip_asgi(app): @app.route("/") def handler(request): - return text("{}".format(request.ip)) + return text("{}".format(request.url)) request, response = await app.asgi_client.get("/") - assert response.text == "mockserver" + assert response.text == "http://mockserver/" def test_text(app): @@ -207,24 +207,24 @@ async def test_empty_json_asgi(app): def test_invalid_json(app): - @app.route("/") + @app.post("/") async def handler(request): return json(request.json) data = "I am not json" - request, response = app.test_client.get("/", data=data) + request, response = app.test_client.post("/", data=data) assert response.status == 400 @pytest.mark.asyncio async def test_invalid_json_asgi(app): - @app.route("/") + @app.post("/") async def handler(request): return json(request.json) data = "I am not json" - request, response = await app.asgi_client.get("/", data=data) + request, response = await app.asgi_client.post("/", data=data) assert response.status == 400 @@ -1807,26 +1807,6 @@ def test_request_port(app): assert hasattr(request, "_port") -@pytest.mark.asyncio -async def test_request_port_asgi(app): - @app.get("/") - def handler(request): - return text("OK") - - request, response = await app.asgi_client.get("/") - - port = request.port - assert isinstance(port, int) - - delattr(request, "_socket") - delattr(request, "_port") - - port = request.port - assert isinstance(port, int) - assert hasattr(request, "_socket") - assert hasattr(request, "_port") - - def test_request_socket(app): @app.get("/") def handler(request): diff --git a/tests/test_utf8.py b/tests/test_utf8.py index 8fd072a4..a2bf893e 100644 --- a/tests/test_utf8.py +++ b/tests/test_utf8.py @@ -37,14 +37,14 @@ def skip_test_utf8_route(app): def test_utf8_post_json(app): - @app.route("/") + @app.post("/") async def handler(request): return text("OK") payload = {"test": "✓"} headers = {"content-type": "application/json"} - request, response = app.test_client.get( + request, response = app.test_client.post( "/", data=json_dumps(payload), headers=headers ) diff --git a/tox.ini b/tox.ini index a1a99268..dcba59f0 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ deps = pytest-sanic pytest-sugar httpcore==0.3.0 - requests-async==0.5.0 + httpx==0.9.3 chardet<=2.3.0 beautifulsoup4 gunicorn From fccbc1adc492e8f805840dd20d7c6391ed49c00f Mon Sep 17 00:00:00 2001 From: Liran Nuna Date: Mon, 23 Dec 2019 12:16:53 -0800 Subject: [PATCH 05/18] Allow empty body without Content-Type; Introduce response.empty() (#1736) --- sanic/response.py | 20 ++++++++++++++++---- tests/test_response.py | 11 +++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 91fb25f4..c75af62e 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -153,7 +153,7 @@ class HTTPResponse(BaseHTTPResponse): body=None, status=200, headers=None, - content_type="text/plain", + content_type=None, body_bytes=b"", ): self.content_type = content_type @@ -181,9 +181,9 @@ class HTTPResponse(BaseHTTPResponse): "Content-Length", len(self.body) ) - self.headers["Content-Type"] = self.headers.get( - "Content-Type", self.content_type - ) + # self.headers get priority over content_type + if self.content_type and "Content-Type" not in self.headers: + self.headers["Content-Type"] = self.content_type if self.status in (304, 412): self.headers = remove_entity_headers(self.headers) @@ -214,6 +214,18 @@ class HTTPResponse(BaseHTTPResponse): return self._cookies +def empty( + status=204, headers=None, +): + """ + Returns an empty response to the client. + + :param status Response code. + :param headers Custom Headers. + """ + return HTTPResponse(body_bytes=b"", status=status, headers=headers,) + + def json( body, status=200, diff --git a/tests/test_response.py b/tests/test_response.py index 8feadb06..87bda1bf 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -21,6 +21,7 @@ from sanic.response import ( raw, stream, ) +from sanic.response import empty from sanic.server import HttpProtocol from sanic.testing import HOST, PORT @@ -591,3 +592,13 @@ def test_raw_response(app): request, response = app.test_client.get("/test") assert response.content_type == "application/octet-stream" assert response.body == b"raw_response" + + +def test_empty_response(app): + @app.get("/test") + def handler(request): + return empty() + + request, response = app.test_client.get("/test") + assert response.content_type is None + assert response.body == b"" From 0a25868a86074aacff099fb2dee65da425ba52e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=2E=20K=C3=A4rkk=C3=A4inen?= <98187+Tronic@users.noreply.github.com> Date: Tue, 24 Dec 2019 01:30:45 +0200 Subject: [PATCH 06/18] HTTP1 header formatting moved to headers.format_headers and rewritten. (#1669) * HTTP1 header formatting moved to headers.format_headers and rewritten. - New implementation is one line of code and twice faster than the old one. - Whole header block encoded to UTF-8 in one pass. - No longer supports custom encode method on header values. - Cookie objects now have __str__ in addition to encode, to work with this. * Add an import missed in merge. --- sanic/cookies.py | 6 +++++- sanic/headers.py | 12 +++++++++++- sanic/response.py | 16 ++-------------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/sanic/cookies.py b/sanic/cookies.py index 19907945..ed672fba 100644 --- a/sanic/cookies.py +++ b/sanic/cookies.py @@ -130,6 +130,10 @@ class Cookie(dict): :return: Cookie encoded in a codec of choosing. :except: UnicodeEncodeError """ + return str(self).encode(encoding) + + def __str__(self): + """Format as a Set-Cookie header value.""" output = ["%s=%s" % (self.key, _quote(self.value))] for key, value in self.items(): if key == "max-age": @@ -147,4 +151,4 @@ class Cookie(dict): else: output.append("%s=%s" % (self._keys[key], value)) - return "; ".join(output).encode(encoding) + return "; ".join(output) diff --git a/sanic/headers.py b/sanic/headers.py index 142ab27b..eef468f9 100644 --- a/sanic/headers.py +++ b/sanic/headers.py @@ -1,9 +1,10 @@ import re -from typing import Dict, Iterable, List, Optional, Tuple, Union +from typing import Any, Dict, Iterable, List, Optional, Tuple, Union from urllib.parse import unquote +HeaderIterable = Iterable[Tuple[str, Any]] # Values convertible to str Options = Dict[str, Union[int, str]] # key=value fields in various headers OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys @@ -170,3 +171,12 @@ def parse_host(host: str) -> Tuple[Optional[str], Optional[int]]: return None, None host, port = m.groups() return host.lower(), int(port) if port is not None else None + + +def format_http1(headers: HeaderIterable) -> bytes: + """Convert a headers iterable into HTTP/1 header format. + + - Outputs UTF-8 bytes where each header line ends with \\r\\n. + - Values are converted into strings if necessary. + """ + return "".join(f"{name}: {val}\r\n" for name, val in headers).encode() diff --git a/sanic/response.py b/sanic/response.py index c75af62e..0b92d2bd 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -7,6 +7,7 @@ from aiofiles import open as open_async # type: ignore from sanic.compat import Header from sanic.cookies import CookieJar +from sanic.headers import format_http1 from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers @@ -30,20 +31,7 @@ class BaseHTTPResponse: return str(data).encode() def _parse_headers(self): - headers = b"" - for name, value in self.headers.items(): - try: - headers += b"%b: %b\r\n" % ( - name.encode(), - value.encode("utf-8"), - ) - except AttributeError: - headers += b"%b: %b\r\n" % ( - str(name).encode(), - str(value).encode("utf-8"), - ) - - return headers + return format_http1(self.headers.items()) @property def cookies(self): From 243f240e5f3f929c9d373e06de390f3177eb7466 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Tue, 24 Dec 2019 01:31:33 +0200 Subject: [PATCH 07/18] Add `RFC` labels to stale exclusion list (#1737) --- .github/stale.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/stale.yml b/.github/stale.yml index 1ddb59e7..15893644 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -8,6 +8,7 @@ exemptLabels: - urgent - necessary - help wanted + - RFC # Label to use when marking an issue as stale staleLabel: stale # Comment to post when marking an issue as stale. Set to `false` to disable From 2b5f8d20dedd4db3325645b470d914bdd526e736 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 25 Dec 2019 16:50:31 -0800 Subject: [PATCH 08/18] ci: Add python nightlies to test matrix (#1710) Signed-off-by: Eli Uriegas --- .travis.yml | 13 +++++++++++++ tox.ini | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4553e325..5980de49 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,6 +43,19 @@ matrix: dist: xenial sudo: true name: "Python 3.7 Documentation tests" + - env: TOX_ENV=pyNightly + python: 'nightly' + name: "Python nightly with Extensions" + - env: TOX_ENV=pyNightly-no-ext + python: 'nightly' + name: "Python nightly Extensions" + allow_failures: + - env: TOX_ENV=pyNightly + python: 'nightly' + name: "Python nightly with Extensions" + - env: TOX_ENV=pyNightly-no-ext + python: 'nightly' + name: "Python nightly Extensions" install: - pip install -U tox - pip install codecov diff --git a/tox.ini b/tox.ini index dcba59f0..d4ed4cb6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,11 @@ [tox] -envlist = py36, py37, {py36,py37}-no-ext, lint, check, security, docs +envlist = py36, py37, pyNightly, {py36,py37,pyNightly}-no-ext, lint, check, security, docs [testenv] usedevelop = True setenv = - {py36,py37}-no-ext: SANIC_NO_UJSON=1 - {py36,py37}-no-ext: SANIC_NO_UVLOOP=1 + {py36,py37,pyNightly}-no-ext: SANIC_NO_UJSON=1 + {py36,py37,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 deps = coverage pytest==5.2.1 From 28899356c878ac222df0beac609ac2a5f211167b Mon Sep 17 00:00:00 2001 From: Stephen Sadowski Date: Thu, 26 Dec 2019 18:47:56 -0600 Subject: [PATCH 09/18] Bumping up version from 19.12.0 to 19.12.0 --- CHANGELOG.rst | 35 +++ Pipfile | 26 +++ Pipfile.lock | 438 +++++++++++++++++++++++++++++++++++++ changelogs/1691.doc.rst | 4 - changelogs/1704.doc.rst | 3 - changelogs/1707.bugfix.rst | 4 - changelogs/37.bugfix.rst | 11 - sanic/__version__.py | 2 +- scripts/pyproject.toml | 2 +- setup.cfg | 2 +- 10 files changed, 502 insertions(+), 25 deletions(-) create mode 100644 Pipfile create mode 100644 Pipfile.lock delete mode 100644 changelogs/1691.doc.rst delete mode 100644 changelogs/1704.doc.rst delete mode 100644 changelogs/1707.bugfix.rst delete mode 100644 changelogs/37.bugfix.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0c9153b1..be827e00 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,38 @@ +Version 19.12.0 +=============== + +Bugfixes +******** + +- Fix blueprint middleware application + + Currently, any blueprint middleware registered, irrespective of which blueprint was used to do so, was + being applied to all of the routes created by the :code:`@app` and :code:`@blueprint` alike. + + As part of this change, the blueprint based middleware application is enforced based on where they are + registered. + + - 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 `__) +- 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 `__) + + +Improved Documentation +********************** + +- Move docs from RST to MD + + 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 `__) +- 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 `__) + + Version 19.6.3 ============== diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000..db71410e --- /dev/null +++ b/Pipfile @@ -0,0 +1,26 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] +towncrier = "*" +requests = "*" +jinja2-time = "*" +multidict = "*" +uvloop = "*" +ujson = "*" +aiofiles = "*" +websockets = "*" +sphinx = "*" +sphinx-rtd-theme = "*" +recommonmark = "*" +httpx = "*" +docutils = "*" +pygments = "*" +httptools = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 00000000..da809077 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,438 @@ +{ + "_meta": { + "hash": { + "sha256": "24c40563a9b7d9a9f53c15d6b50eb81384983cccbe1a6f97f8b0026df54efa4c" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aiofiles": { + "hashes": [ + "sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee", + "sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d" + ], + "index": "pypi", + "version": "==0.4.0" + }, + "alabaster": { + "hashes": [ + "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", + "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" + ], + "version": "==0.7.12" + }, + "arrow": { + "hashes": [ + "sha256:01a16d8a93eddf86a29237f32ae36b29c27f047e79312eb4df5d55fd5a2b3183", + "sha256:e1a318a4c0b787833ae46302c02488b6eeef413c6a13324b3261ad320f21ec1e" + ], + "version": "==0.15.4" + }, + "babel": { + "hashes": [ + "sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab", + "sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28" + ], + "version": "==2.7.0" + }, + "certifi": { + "hashes": [ + "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", + "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" + ], + "version": "==2019.11.28" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "commonmark": { + "hashes": [ + "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", + "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9" + ], + "version": "==0.9.1" + }, + "docutils": { + "hashes": [ + "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", + "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", + "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" + ], + "index": "pypi", + "version": "==0.15.2" + }, + "h11": { + "hashes": [ + "sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208", + "sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7" + ], + "version": "==0.8.1" + }, + "h2": { + "hashes": [ + "sha256:ac377fcf586314ef3177bfd90c12c7826ab0840edeb03f0f24f511858326049e", + "sha256:b8a32bd282594424c0ac55845377eea13fa54fe4a8db012f3a198ed923dc3ab4" + ], + "version": "==3.1.1" + }, + "hpack": { + "hashes": [ + "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89", + "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2" + ], + "version": "==3.0.0" + }, + "hstspreload": { + "hashes": [ + "sha256:44e231c0111de4ed019fae7b5bfbc9071c1e7c9887df9602fb2e076c68a92f7e" + ], + "version": "==2019.12.25" + }, + "httptools": { + "hashes": [ + "sha256:e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc" + ], + "index": "pypi", + "version": "==0.0.13" + }, + "httpx": { + "hashes": [ + "sha256:0ea1ec7da0616eb74cadc4d4dc8e0c2cde0fcdb2d17bbc3e3d05153d52087139", + "sha256:3f277b2a68c5d5fd83d89f80fec811ea6eb1c3c9e47a26f7ecc294c255b10b8d" + ], + "index": "pypi", + "version": "==0.9.5" + }, + "hyperframe": { + "hashes": [ + "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40", + "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f" + ], + "version": "==5.2.0" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "imagesize": { + "hashes": [ + "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", + "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" + ], + "version": "==1.2.0" + }, + "incremental": { + "hashes": [ + "sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f", + "sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3" + ], + "version": "==17.5.0" + }, + "jinja2": { + "hashes": [ + "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", + "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" + ], + "version": "==2.10.3" + }, + "jinja2-time": { + "hashes": [ + "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40", + "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa" + ], + "index": "pypi", + "version": "==0.2.0" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + ], + "version": "==1.1.1" + }, + "multidict": { + "hashes": [ + "sha256:20081b14c923d2c5122c13e060d0ee334e078e1802c36006b20c8d7a59ee6a52", + "sha256:335344a3c3b19845c73a7826f359c51c4a12a1ccd2392b5f572a63b452bfc771", + "sha256:49e80c53659c7ac50ec1c4b5fa50f045b67fffeb5b735dccb6205e4ff122e8b6", + "sha256:615a282acd530a1bc1b01f069a8c5874cb7c2780c287a2895ad5ab7407540e9d", + "sha256:63d9a3d93a514549760cb68c82864966bddb6ab53a3326931c8db9ad29414603", + "sha256:77264002c184538df5dcb4fc1de5df6803587fa30bbe12203a7a3436b8aafc0f", + "sha256:7dd6f6a51b64d0a6473bc30c53e1d73fcb461c437f43662b7d6d701bd90db253", + "sha256:7f4e591ec80619e74c50b7800f9db9b0e01d2099094767050dfe2e78e1c41839", + "sha256:824716bba5a4efd74ddd36a3830efb9e49860149514ef7c41aac0144757ebb5d", + "sha256:8f30ead697c2e37147d82ba8019952b5ea99bd3d1052f1f1ebff951eaa953209", + "sha256:a03efe8b7591c77d9ad4b9d81dcfb9c96e538ae25eb114385f35f4d7ffa3bac2", + "sha256:b86e8e33a0a24240b293e7fad233a7e886bae6e51ca6923d39f4e313dd1d5578", + "sha256:c1c64c93b8754a5cebd495d136f47a5ca93cbfceba532e306a768c27a0c1292b", + "sha256:d4dafdcfbf0ac80fc5f00603f0ce43e487c654ae34a656e4749f175d9832b1b5", + "sha256:daf6d89e47418e38af98e1f2beb3fe0c8aa34806f681d04df314c0f131dcf01d", + "sha256:e03b7addca96b9eb24d6eabbdc3041e8f71fd47b316e0f3c0fa993fc7b99002c", + "sha256:ff53a434890a16356bc45c0b90557efd89d0e5a820dbab37015d7ee630c6707a" + ], + "index": "pypi", + "version": "==4.7.2" + }, + "packaging": { + "hashes": [ + "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", + "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" + ], + "version": "==19.2" + }, + "pygments": { + "hashes": [ + "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", + "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe" + ], + "index": "pypi", + "version": "==2.5.2" + }, + "pyparsing": { + "hashes": [ + "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", + "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" + ], + "version": "==2.4.6" + }, + "python-dateutil": { + "hashes": [ + "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", + "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" + ], + "version": "==2.8.1" + }, + "pytz": { + "hashes": [ + "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", + "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" + ], + "version": "==2019.3" + }, + "recommonmark": { + "hashes": [ + "sha256:29cd4faeb6c5268c633634f2d69aef9431e0f4d347f90659fd0aab20e541efeb", + "sha256:2ec4207a574289355d5b6ae4ae4abb29043346ca12cdd5f07d374dc5987d2852" + ], + "index": "pypi", + "version": "==0.6.0" + }, + "requests": { + "hashes": [ + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + ], + "index": "pypi", + "version": "==2.22.0" + }, + "rfc3986": { + "hashes": [ + "sha256:0344d0bd428126ce554e7ca2b61787b6a28d2bbd19fc70ed2dd85efe31176405", + "sha256:df4eba676077cefb86450c8f60121b9ae04b94f65f85b69f3f731af0516b7b18" + ], + "version": "==1.3.2" + }, + "six": { + "hashes": [ + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + ], + "version": "==1.13.0" + }, + "sniffio": { + "hashes": [ + "sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5", + "sha256:8e3810100f69fe0edd463d02ad407112542a11ffdc29f67db2bf3771afb87a21" + ], + "version": "==1.1.0" + }, + "snowballstemmer": { + "hashes": [ + "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", + "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52" + ], + "version": "==2.0.0" + }, + "sphinx": { + "hashes": [ + "sha256:298537cb3234578b2d954ff18c5608468229e116a9757af3b831c2b2b4819159", + "sha256:e6e766b74f85f37a5f3e0773a1e1be8db3fcb799deb58ca6d18b70b0b44542a5" + ], + "index": "pypi", + "version": "==2.3.1" + }, + "sphinx-rtd-theme": { + "hashes": [ + "sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4", + "sha256:728607e34d60456d736cc7991fd236afb828b21b82f956c5ea75f94c8414040a" + ], + "index": "pypi", + "version": "==0.4.3" + }, + "sphinxcontrib-applehelp": { + "hashes": [ + "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", + "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d" + ], + "version": "==1.0.1" + }, + "sphinxcontrib-devhelp": { + "hashes": [ + "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", + "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981" + ], + "version": "==1.0.1" + }, + "sphinxcontrib-htmlhelp": { + "hashes": [ + "sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422", + "sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7" + ], + "version": "==1.0.2" + }, + "sphinxcontrib-jsmath": { + "hashes": [ + "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", + "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" + ], + "version": "==1.0.1" + }, + "sphinxcontrib-qthelp": { + "hashes": [ + "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", + "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f" + ], + "version": "==1.0.2" + }, + "sphinxcontrib-serializinghtml": { + "hashes": [ + "sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", + "sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768" + ], + "version": "==1.1.3" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "towncrier": { + "hashes": [ + "sha256:48251a1ae66d2cf7e6fa5552016386831b3e12bb3b2d08eb70374508c17a8196", + "sha256:de19da8b8cb44f18ea7ed3a3823087d2af8fcf497151bb9fd1e1b092ff56ed8d" + ], + "index": "pypi", + "version": "==19.2.0" + }, + "ujson": { + "hashes": [ + "sha256:f66073e5506e91d204ab0c614a148d5aa938bdbf104751be66f8ad7a222f5f86" + ], + "index": "pypi", + "version": "==1.35" + }, + "urllib3": { + "hashes": [ + "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", + "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" + ], + "version": "==1.25.7" + }, + "uvloop": { + "hashes": [ + "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", + "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e", + "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09", + "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726", + "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891", + "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7", + "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5", + "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", + "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" + ], + "index": "pypi", + "version": "==0.14.0" + }, + "websockets": { + "hashes": [ + "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", + "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", + "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", + "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", + "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", + "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", + "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", + "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", + "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", + "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", + "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", + "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", + "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", + "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", + "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", + "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", + "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", + "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", + "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", + "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", + "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", + "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" + ], + "index": "pypi", + "version": "==8.1" + } + }, + "develop": {} +} diff --git a/changelogs/1691.doc.rst b/changelogs/1691.doc.rst deleted file mode 100644 index e4a9e3de..00000000 --- a/changelogs/1691.doc.rst +++ /dev/null @@ -1,4 +0,0 @@ -Move docs from RST to MD - -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. diff --git a/changelogs/1704.doc.rst b/changelogs/1704.doc.rst deleted file mode 100644 index 873ce539..00000000 --- a/changelogs/1704.doc.rst +++ /dev/null @@ -1,3 +0,0 @@ -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 diff --git a/changelogs/1707.bugfix.rst b/changelogs/1707.bugfix.rst deleted file mode 100644 index a98cd6b4..00000000 --- a/changelogs/1707.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -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. diff --git a/changelogs/37.bugfix.rst b/changelogs/37.bugfix.rst deleted file mode 100644 index 80c00a3f..00000000 --- a/changelogs/37.bugfix.rst +++ /dev/null @@ -1,11 +0,0 @@ -Fix blueprint middleware application - -Currently, any blueprint middleware registered, irrespective of which blueprint was used to do so, was -being applied to all of the routes created by the :code:`@app` and :code:`@blueprint` alike. - -As part of this change, the blueprint based middleware application is enforced based on where they are -registered. - -- 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 diff --git a/sanic/__version__.py b/sanic/__version__.py index 08f1a61d..3124890b 100644 --- a/sanic/__version__.py +++ b/sanic/__version__.py @@ -1 +1 @@ -__version__ = "19.9.0" +__version__ = "19.12.0" diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml index 34acf03f..0b2a417d 100644 --- a/scripts/pyproject.toml +++ b/scripts/pyproject.toml @@ -1,6 +1,6 @@ [tool.towncrier] package = "sanic" -package_dir = "." +package_dir = ".." filename = "../CHANGELOG.rst" directory = "./changelogs" underlines = ["=", "*", "~"] diff --git a/setup.cfg b/setup.cfg index d0efa01a..ac75014e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,7 @@ multi_line_output = 3 not_skip = __init__.py [version] -current_version = 19.9.0 +current_version = 19.12.0 files = sanic/__version__.py current_version_pattern = __version__ = "{current_version}" new_version_pattern = __version__ = "{new_version}" From 3411a12c40a4df7ffdb555b2767ed0fcc2b0971c Mon Sep 17 00:00:00 2001 From: Stephen Sadowski Date: Thu, 26 Dec 2019 18:50:52 -0600 Subject: [PATCH 10/18] Pipfile crud removed --- Pipfile | 26 --- Pipfile.lock | 438 --------------------------------------------------- 2 files changed, 464 deletions(-) delete mode 100644 Pipfile delete mode 100644 Pipfile.lock diff --git a/Pipfile b/Pipfile deleted file mode 100644 index db71410e..00000000 --- a/Pipfile +++ /dev/null @@ -1,26 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] - -[packages] -towncrier = "*" -requests = "*" -jinja2-time = "*" -multidict = "*" -uvloop = "*" -ujson = "*" -aiofiles = "*" -websockets = "*" -sphinx = "*" -sphinx-rtd-theme = "*" -recommonmark = "*" -httpx = "*" -docutils = "*" -pygments = "*" -httptools = "*" - -[requires] -python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index da809077..00000000 --- a/Pipfile.lock +++ /dev/null @@ -1,438 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "24c40563a9b7d9a9f53c15d6b50eb81384983cccbe1a6f97f8b0026df54efa4c" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "aiofiles": { - "hashes": [ - "sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee", - "sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d" - ], - "index": "pypi", - "version": "==0.4.0" - }, - "alabaster": { - "hashes": [ - "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", - "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" - ], - "version": "==0.7.12" - }, - "arrow": { - "hashes": [ - "sha256:01a16d8a93eddf86a29237f32ae36b29c27f047e79312eb4df5d55fd5a2b3183", - "sha256:e1a318a4c0b787833ae46302c02488b6eeef413c6a13324b3261ad320f21ec1e" - ], - "version": "==0.15.4" - }, - "babel": { - "hashes": [ - "sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab", - "sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28" - ], - "version": "==2.7.0" - }, - "certifi": { - "hashes": [ - "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", - "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" - ], - "version": "==2019.11.28" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "click": { - "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" - ], - "version": "==7.0" - }, - "commonmark": { - "hashes": [ - "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", - "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9" - ], - "version": "==0.9.1" - }, - "docutils": { - "hashes": [ - "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", - "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", - "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" - ], - "index": "pypi", - "version": "==0.15.2" - }, - "h11": { - "hashes": [ - "sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208", - "sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7" - ], - "version": "==0.8.1" - }, - "h2": { - "hashes": [ - "sha256:ac377fcf586314ef3177bfd90c12c7826ab0840edeb03f0f24f511858326049e", - "sha256:b8a32bd282594424c0ac55845377eea13fa54fe4a8db012f3a198ed923dc3ab4" - ], - "version": "==3.1.1" - }, - "hpack": { - "hashes": [ - "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89", - "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2" - ], - "version": "==3.0.0" - }, - "hstspreload": { - "hashes": [ - "sha256:44e231c0111de4ed019fae7b5bfbc9071c1e7c9887df9602fb2e076c68a92f7e" - ], - "version": "==2019.12.25" - }, - "httptools": { - "hashes": [ - "sha256:e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc" - ], - "index": "pypi", - "version": "==0.0.13" - }, - "httpx": { - "hashes": [ - "sha256:0ea1ec7da0616eb74cadc4d4dc8e0c2cde0fcdb2d17bbc3e3d05153d52087139", - "sha256:3f277b2a68c5d5fd83d89f80fec811ea6eb1c3c9e47a26f7ecc294c255b10b8d" - ], - "index": "pypi", - "version": "==0.9.5" - }, - "hyperframe": { - "hashes": [ - "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40", - "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f" - ], - "version": "==5.2.0" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "imagesize": { - "hashes": [ - "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", - "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" - ], - "version": "==1.2.0" - }, - "incremental": { - "hashes": [ - "sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f", - "sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3" - ], - "version": "==17.5.0" - }, - "jinja2": { - "hashes": [ - "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", - "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" - ], - "version": "==2.10.3" - }, - "jinja2-time": { - "hashes": [ - "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40", - "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa" - ], - "index": "pypi", - "version": "==0.2.0" - }, - "markupsafe": { - "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" - ], - "version": "==1.1.1" - }, - "multidict": { - "hashes": [ - "sha256:20081b14c923d2c5122c13e060d0ee334e078e1802c36006b20c8d7a59ee6a52", - "sha256:335344a3c3b19845c73a7826f359c51c4a12a1ccd2392b5f572a63b452bfc771", - "sha256:49e80c53659c7ac50ec1c4b5fa50f045b67fffeb5b735dccb6205e4ff122e8b6", - "sha256:615a282acd530a1bc1b01f069a8c5874cb7c2780c287a2895ad5ab7407540e9d", - "sha256:63d9a3d93a514549760cb68c82864966bddb6ab53a3326931c8db9ad29414603", - "sha256:77264002c184538df5dcb4fc1de5df6803587fa30bbe12203a7a3436b8aafc0f", - "sha256:7dd6f6a51b64d0a6473bc30c53e1d73fcb461c437f43662b7d6d701bd90db253", - "sha256:7f4e591ec80619e74c50b7800f9db9b0e01d2099094767050dfe2e78e1c41839", - "sha256:824716bba5a4efd74ddd36a3830efb9e49860149514ef7c41aac0144757ebb5d", - "sha256:8f30ead697c2e37147d82ba8019952b5ea99bd3d1052f1f1ebff951eaa953209", - "sha256:a03efe8b7591c77d9ad4b9d81dcfb9c96e538ae25eb114385f35f4d7ffa3bac2", - "sha256:b86e8e33a0a24240b293e7fad233a7e886bae6e51ca6923d39f4e313dd1d5578", - "sha256:c1c64c93b8754a5cebd495d136f47a5ca93cbfceba532e306a768c27a0c1292b", - "sha256:d4dafdcfbf0ac80fc5f00603f0ce43e487c654ae34a656e4749f175d9832b1b5", - "sha256:daf6d89e47418e38af98e1f2beb3fe0c8aa34806f681d04df314c0f131dcf01d", - "sha256:e03b7addca96b9eb24d6eabbdc3041e8f71fd47b316e0f3c0fa993fc7b99002c", - "sha256:ff53a434890a16356bc45c0b90557efd89d0e5a820dbab37015d7ee630c6707a" - ], - "index": "pypi", - "version": "==4.7.2" - }, - "packaging": { - "hashes": [ - "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", - "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" - ], - "version": "==19.2" - }, - "pygments": { - "hashes": [ - "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", - "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe" - ], - "index": "pypi", - "version": "==2.5.2" - }, - "pyparsing": { - "hashes": [ - "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", - "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" - ], - "version": "==2.4.6" - }, - "python-dateutil": { - "hashes": [ - "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", - "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" - ], - "version": "==2.8.1" - }, - "pytz": { - "hashes": [ - "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", - "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" - ], - "version": "==2019.3" - }, - "recommonmark": { - "hashes": [ - "sha256:29cd4faeb6c5268c633634f2d69aef9431e0f4d347f90659fd0aab20e541efeb", - "sha256:2ec4207a574289355d5b6ae4ae4abb29043346ca12cdd5f07d374dc5987d2852" - ], - "index": "pypi", - "version": "==0.6.0" - }, - "requests": { - "hashes": [ - "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", - "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" - ], - "index": "pypi", - "version": "==2.22.0" - }, - "rfc3986": { - "hashes": [ - "sha256:0344d0bd428126ce554e7ca2b61787b6a28d2bbd19fc70ed2dd85efe31176405", - "sha256:df4eba676077cefb86450c8f60121b9ae04b94f65f85b69f3f731af0516b7b18" - ], - "version": "==1.3.2" - }, - "six": { - "hashes": [ - "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", - "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" - ], - "version": "==1.13.0" - }, - "sniffio": { - "hashes": [ - "sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5", - "sha256:8e3810100f69fe0edd463d02ad407112542a11ffdc29f67db2bf3771afb87a21" - ], - "version": "==1.1.0" - }, - "snowballstemmer": { - "hashes": [ - "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", - "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52" - ], - "version": "==2.0.0" - }, - "sphinx": { - "hashes": [ - "sha256:298537cb3234578b2d954ff18c5608468229e116a9757af3b831c2b2b4819159", - "sha256:e6e766b74f85f37a5f3e0773a1e1be8db3fcb799deb58ca6d18b70b0b44542a5" - ], - "index": "pypi", - "version": "==2.3.1" - }, - "sphinx-rtd-theme": { - "hashes": [ - "sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4", - "sha256:728607e34d60456d736cc7991fd236afb828b21b82f956c5ea75f94c8414040a" - ], - "index": "pypi", - "version": "==0.4.3" - }, - "sphinxcontrib-applehelp": { - "hashes": [ - "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", - "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d" - ], - "version": "==1.0.1" - }, - "sphinxcontrib-devhelp": { - "hashes": [ - "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", - "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981" - ], - "version": "==1.0.1" - }, - "sphinxcontrib-htmlhelp": { - "hashes": [ - "sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422", - "sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7" - ], - "version": "==1.0.2" - }, - "sphinxcontrib-jsmath": { - "hashes": [ - "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", - "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" - ], - "version": "==1.0.1" - }, - "sphinxcontrib-qthelp": { - "hashes": [ - "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", - "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f" - ], - "version": "==1.0.2" - }, - "sphinxcontrib-serializinghtml": { - "hashes": [ - "sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", - "sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768" - ], - "version": "==1.1.3" - }, - "toml": { - "hashes": [ - "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" - ], - "version": "==0.10.0" - }, - "towncrier": { - "hashes": [ - "sha256:48251a1ae66d2cf7e6fa5552016386831b3e12bb3b2d08eb70374508c17a8196", - "sha256:de19da8b8cb44f18ea7ed3a3823087d2af8fcf497151bb9fd1e1b092ff56ed8d" - ], - "index": "pypi", - "version": "==19.2.0" - }, - "ujson": { - "hashes": [ - "sha256:f66073e5506e91d204ab0c614a148d5aa938bdbf104751be66f8ad7a222f5f86" - ], - "index": "pypi", - "version": "==1.35" - }, - "urllib3": { - "hashes": [ - "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", - "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" - ], - "version": "==1.25.7" - }, - "uvloop": { - "hashes": [ - "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", - "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e", - "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09", - "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726", - "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891", - "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7", - "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5", - "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", - "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" - ], - "index": "pypi", - "version": "==0.14.0" - }, - "websockets": { - "hashes": [ - "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", - "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", - "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", - "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", - "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", - "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", - "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", - "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", - "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", - "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", - "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", - "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", - "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", - "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", - "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", - "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", - "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", - "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", - "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", - "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", - "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", - "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" - ], - "index": "pypi", - "version": "==8.1" - } - }, - "develop": {} -} From 075affec23084f68a2a5817521d3cecdb25cd152 Mon Sep 17 00:00:00 2001 From: Stephen Sadowski Date: Fri, 27 Dec 2019 07:10:46 -0600 Subject: [PATCH 11/18] Release v19.12.0 (#1740) * Bumping up version from 19.9.0 to 19.12.0 * Pipfile crud removed --- CHANGELOG.rst | 35 +++++++++++++++++++++++++++++++++++ changelogs/1691.doc.rst | 4 ---- changelogs/1704.doc.rst | 3 --- changelogs/1707.bugfix.rst | 4 ---- changelogs/37.bugfix.rst | 11 ----------- sanic/__version__.py | 2 +- scripts/pyproject.toml | 2 +- setup.cfg | 2 +- 8 files changed, 38 insertions(+), 25 deletions(-) delete mode 100644 changelogs/1691.doc.rst delete mode 100644 changelogs/1704.doc.rst delete mode 100644 changelogs/1707.bugfix.rst delete mode 100644 changelogs/37.bugfix.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0c9153b1..be827e00 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,38 @@ +Version 19.12.0 +=============== + +Bugfixes +******** + +- Fix blueprint middleware application + + Currently, any blueprint middleware registered, irrespective of which blueprint was used to do so, was + being applied to all of the routes created by the :code:`@app` and :code:`@blueprint` alike. + + As part of this change, the blueprint based middleware application is enforced based on where they are + registered. + + - 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 `__) +- 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 `__) + + +Improved Documentation +********************** + +- Move docs from RST to MD + + 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 `__) +- 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 `__) + + Version 19.6.3 ============== diff --git a/changelogs/1691.doc.rst b/changelogs/1691.doc.rst deleted file mode 100644 index e4a9e3de..00000000 --- a/changelogs/1691.doc.rst +++ /dev/null @@ -1,4 +0,0 @@ -Move docs from RST to MD - -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. diff --git a/changelogs/1704.doc.rst b/changelogs/1704.doc.rst deleted file mode 100644 index 873ce539..00000000 --- a/changelogs/1704.doc.rst +++ /dev/null @@ -1,3 +0,0 @@ -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 diff --git a/changelogs/1707.bugfix.rst b/changelogs/1707.bugfix.rst deleted file mode 100644 index a98cd6b4..00000000 --- a/changelogs/1707.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -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. diff --git a/changelogs/37.bugfix.rst b/changelogs/37.bugfix.rst deleted file mode 100644 index 80c00a3f..00000000 --- a/changelogs/37.bugfix.rst +++ /dev/null @@ -1,11 +0,0 @@ -Fix blueprint middleware application - -Currently, any blueprint middleware registered, irrespective of which blueprint was used to do so, was -being applied to all of the routes created by the :code:`@app` and :code:`@blueprint` alike. - -As part of this change, the blueprint based middleware application is enforced based on where they are -registered. - -- 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 diff --git a/sanic/__version__.py b/sanic/__version__.py index 08f1a61d..3124890b 100644 --- a/sanic/__version__.py +++ b/sanic/__version__.py @@ -1 +1 @@ -__version__ = "19.9.0" +__version__ = "19.12.0" diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml index 34acf03f..0b2a417d 100644 --- a/scripts/pyproject.toml +++ b/scripts/pyproject.toml @@ -1,6 +1,6 @@ [tool.towncrier] package = "sanic" -package_dir = "." +package_dir = ".." filename = "../CHANGELOG.rst" directory = "./changelogs" underlines = ["=", "*", "~"] diff --git a/setup.cfg b/setup.cfg index d0efa01a..ac75014e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,7 @@ multi_line_output = 3 not_skip = __init__.py [version] -current_version = 19.9.0 +current_version = 19.12.0 files = sanic/__version__.py current_version_pattern = __version__ = "{current_version}" new_version_pattern = __version__ = "{new_version}" From a9c669f17b24a812b7bbad4a148df9ccc758c05f Mon Sep 17 00:00:00 2001 From: Eric Nieuwland Date: Sat, 28 Dec 2019 15:21:27 +0100 Subject: [PATCH 12/18] Forgotten slot Crashes the server at __init__() time --- sanic/server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sanic/server.py b/sanic/server.py index bde6def7..2e6be4a5 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -87,6 +87,7 @@ class HttpProtocol(asyncio.Protocol): "_header_fragment", "state", "_debug", + "_body_chunks", ) def __init__( From 343090704641c2380d98bdb1e068574e466cc186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=BD=E9=A3=8E?= Date: Sat, 4 Jan 2020 12:20:42 +0800 Subject: [PATCH 13/18] fix 1748 : Drop DeprecationWarning in python 3.8 (#1750) https://github.com/huge-success/sanic/issues/1748 --- sanic/server.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 2e6be4a5..47ebcd95 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -1,5 +1,6 @@ import asyncio import os +import sys import traceback from collections import deque @@ -134,7 +135,10 @@ class HttpProtocol(asyncio.Protocol): self.request_class = request_class or Request self.is_request_stream = is_request_stream self._is_stream_handler = False - self._not_paused = asyncio.Event(loop=loop) + if sys.version_info.minor >= 8: + self._not_paused = asyncio.Event() + else: + self._not_paused = asyncio.Event(loop=loop) self._total_request_size = 0 self._request_timeout_handler = None self._response_timeout_handler = None @@ -951,7 +955,10 @@ def serve( else: conn.close() - _shutdown = asyncio.gather(*coros, loop=loop) + if sys.version_info.minor >= 8: + _shutdown = asyncio.gather(*coros, loop=loop) + else: + _shutdown = asyncio.gather(*coros) loop.run_until_complete(_shutdown) trigger_events(after_stop, loop) From cd779b6e4fab4e856709b96f8b39dd1144bf72e3 Mon Sep 17 00:00:00 2001 From: Lagicrus Date: Sat, 4 Jan 2020 19:51:50 +0000 Subject: [PATCH 14/18] Update response.rst --- docs/sanic/response.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/sanic/response.rst b/docs/sanic/response.rst index 75a44425..6fb487b2 100644 --- a/docs/sanic/response.rst +++ b/docs/sanic/response.rst @@ -107,6 +107,19 @@ Response without encoding the body def handle_request(request): return response.raw(b'raw data') +Empty +-------------- + +For responding with a empty message as defined by `RFC 2616 `_ + +.. code-block:: python + + from sanic import response + + @app.route('/empty') + async def handle_request(request): + return response.empty() + Modify headers or status ------------------------ From 0fd08c6114a62541a2c060ad2b4279208b1234f7 Mon Sep 17 00:00:00 2001 From: Lagicrus Date: Sat, 4 Jan 2020 21:26:03 +0000 Subject: [PATCH 15/18] Update response.rst --- docs/sanic/response.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sanic/response.rst b/docs/sanic/response.rst index 6fb487b2..a241b7ac 100644 --- a/docs/sanic/response.rst +++ b/docs/sanic/response.rst @@ -110,7 +110,7 @@ Response without encoding the body Empty -------------- -For responding with a empty message as defined by `RFC 2616 `_ +For responding with an empty message as defined by `RFC 2616 `_ .. code-block:: python From 865536c5c4f79aa0516775c659ea878a986ce8db Mon Sep 17 00:00:00 2001 From: Liran Nuna Date: Fri, 10 Jan 2020 06:43:44 -0800 Subject: [PATCH 16/18] Simplify status code to text lookup (#1738) --- sanic/response.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 0b92d2bd..36481618 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -118,12 +118,7 @@ class StreamingHTTPResponse(BaseHTTPResponse): ) headers = self._parse_headers() - - if self.status == 200: - status = b"OK" - else: - status = STATUS_CODES.get(self.status) - + status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE") return (b"HTTP/%b %d %b\r\n" b"%b" b"%b\r\n") % ( version.encode(), self.status, @@ -177,12 +172,7 @@ class HTTPResponse(BaseHTTPResponse): self.headers = remove_entity_headers(self.headers) headers = self._parse_headers() - - if self.status == 200: - status = b"OK" - else: - status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE") - + status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE") return ( b"HTTP/%b %d %b\r\n" b"Connection: %b\r\n" b"%b" b"%b\r\n" b"%b" ) % ( From caa1b4d69b8b154704c264d154b0579faa6b2bb3 Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Sat, 11 Jan 2020 13:58:01 +1000 Subject: [PATCH 17/18] Fix dangling comma in arguments list for HTTPResponse in response.empty() (#1761) * Fix dangling comma arguments list for HTTPResponse in response.empty() * Found another black error, including another dangling comma --- sanic/response.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 36481618..41650412 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -192,16 +192,14 @@ class HTTPResponse(BaseHTTPResponse): return self._cookies -def empty( - status=204, headers=None, -): +def empty(status=204, headers=None): """ Returns an empty response to the client. :param status Response code. :param headers Custom Headers. """ - return HTTPResponse(body_bytes=b"", status=status, headers=headers,) + return HTTPResponse(body_bytes=b"", status=status, headers=headers) def json( From b565072ed9792debb017ba3dc2b3de67f8f03514 Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Sat, 11 Jan 2020 15:50:16 +1000 Subject: [PATCH 18/18] Allow route decorators to stack up again (#1764) * Allow route decorators to stack up without causing a function signature inspection crash Fix #1742 * Apply fix to @websocket routes docorator too Add test for double-stacked websocket decorator remove introduction of new variable in route wrapper, extend routes in-place. Add explanation of why a handler will be a tuple in the case of a double-stacked route decorator --- sanic/app.py | 47 ++++++++++++++++++++++++++++++-------------- tests/test_routes.py | 29 +++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 65e84809..abdd36fb 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -194,6 +194,12 @@ class Sanic: strict_slashes = self.strict_slashes def response(handler): + if isinstance(handler, tuple): + # if a handler fn is already wrapped in a route, the handler + # variable will be a tuple of (existing routes, handler fn) + routes, handler = handler + else: + routes = [] args = list(signature(handler).parameters.keys()) if not args: @@ -205,14 +211,16 @@ class Sanic: if stream: handler.is_stream = stream - routes = self.router.add( - uri=uri, - methods=methods, - handler=handler, - host=host, - strict_slashes=strict_slashes, - version=version, - name=name, + routes.extend( + self.router.add( + uri=uri, + methods=methods, + handler=handler, + host=host, + strict_slashes=strict_slashes, + version=version, + name=name, + ) ) return routes, handler @@ -476,6 +484,13 @@ class Sanic: strict_slashes = self.strict_slashes def response(handler): + if isinstance(handler, tuple): + # if a handler fn is already wrapped in a route, the handler + # variable will be a tuple of (existing routes, handler fn) + routes, handler = handler + else: + routes = [] + async def websocket_handler(request, *args, **kwargs): request.app = self if not getattr(handler, "__blueprintname__", False): @@ -516,13 +531,15 @@ class Sanic: self.websocket_tasks.remove(fut) await ws.close() - routes = self.router.add( - uri=uri, - handler=websocket_handler, - methods=frozenset({"GET"}), - host=host, - strict_slashes=strict_slashes, - name=name, + routes.extend( + self.router.add( + uri=uri, + handler=websocket_handler, + methods=frozenset({"GET"}), + host=host, + strict_slashes=strict_slashes, + name=name, + ) ) return routes, handler diff --git a/tests/test_routes.py b/tests/test_routes.py index 3b24389f..31fa1a56 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -551,6 +551,35 @@ def test_route_duplicate(app): pass +def test_double_stack_route(app): + @app.route("/test/1") + @app.route("/test/2") + async def handler1(request): + return text("OK") + + request, response = app.test_client.get("/test/1") + assert response.status == 200 + request, response = app.test_client.get("/test/2") + assert response.status == 200 + + +@pytest.mark.asyncio +async def test_websocket_route_asgi(app): + ev = asyncio.Event() + + @app.websocket("/test/1") + @app.websocket("/test/2") + async def handler(request, ws): + ev.set() + + request, response = await app.asgi_client.websocket("/test/1") + first_set = ev.is_set() + ev.clear() + request, response = await app.asgi_client.websocket("/test/1") + second_set = ev.is_set() + assert(first_set and second_set) + + def test_method_not_allowed(app): @app.route("/test", methods=["GET"]) async def handler(request):