From becbc5f9efb9907e2c97b699d30bd258571c1390 Mon Sep 17 00:00:00 2001 From: fanjindong <765912710@qq.com> Date: Wed, 11 Jul 2018 16:42:34 +0800 Subject: [PATCH 01/32] fix one example and add one example (#1257) --- examples/blueprints.py | 13 ++++++----- examples/simple_async_view.py | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 examples/simple_async_view.py diff --git a/examples/blueprints.py b/examples/blueprints.py index 03154049..29144c4e 100644 --- a/examples/blueprints.py +++ b/examples/blueprints.py @@ -1,7 +1,5 @@ -from sanic import Sanic -from sanic import Blueprint -from sanic.response import json - +from sanic import Blueprint, Sanic +from sanic.response import file, json app = Sanic(__name__) blueprint = Blueprint('name', url_prefix='/my_blueprint') @@ -19,7 +17,12 @@ async def foo2(request): return json({'msg': 'hi from blueprint2'}) -@blueprint3.websocket('/foo') +@blueprint3.route('/foo') +async def index(request): + return await file('websocket.html') + + +@app.websocket('/feed') async def foo3(request, ws): while True: data = 'hello!' diff --git a/examples/simple_async_view.py b/examples/simple_async_view.py new file mode 100644 index 00000000..990aa21a --- /dev/null +++ b/examples/simple_async_view.py @@ -0,0 +1,42 @@ +from sanic import Sanic +from sanic.views import HTTPMethodView +from sanic.response import text + +app = Sanic('some_name') + + +class SimpleView(HTTPMethodView): + + def get(self, request): + return text('I am get method') + + def post(self, request): + return text('I am post method') + + def put(self, request): + return text('I am put method') + + def patch(self, request): + return text('I am patch method') + + def delete(self, request): + return text('I am delete method') + + +class SimpleAsyncView(HTTPMethodView): + + async def get(self, request): + return text('I am async get method') + + async def post(self, request): + return text('I am async post method') + + async def put(self, request): + return text('I am async put method') + + +app.add_route(SimpleView.as_view(), '/') +app.add_route(SimpleAsyncView.as_view(), '/async') + +if __name__ == '__main__': + app.run(host="0.0.0.0", port=8000, debug=True) From 334649dfd4e9b16cbcadcc6264ac9dee6ab7baf3 Mon Sep 17 00:00:00 2001 From: 7 Date: Wed, 11 Jul 2018 01:44:21 -0700 Subject: [PATCH 02/32] Fix response ci header (#1244) * add unit tests, which should fail * fix CIDict * moving CIDict to avoid circular imports * fix unit tests * use multidict for headers * fix cookie * add version constraint for multidict * omit test coverage for __main__.py * make flake8 happy * consolidate check in for loop * travisci retry build --- .coveragerc | 2 +- requirements-dev.txt | 1 + requirements.txt | 1 + sanic/cookies.py | 30 +++++++++--------------------- sanic/response.py | 5 +++-- sanic/server.py | 22 ++-------------------- setup.py | 1 + tests/test_cookies.py | 1 + tests/test_response.py | 19 +++++++++++++++++++ 9 files changed, 38 insertions(+), 44 deletions(-) diff --git a/.coveragerc b/.coveragerc index f0624d2e..724b2872 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,7 +1,7 @@ [run] branch = True source = sanic -omit = site-packages, sanic/utils.py +omit = site-packages, sanic/utils.py, sanic/__main__.py [html] directory = coverage diff --git a/requirements-dev.txt b/requirements-dev.txt index 674ef91d..3d94c51d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,3 +10,4 @@ tox ujson; sys_platform != "win32" and implementation_name == "cpython" uvloop; sys_platform != "win32" and implementation_name == "cpython" gunicorn +multidict>=4.0,<5.0 diff --git a/requirements.txt b/requirements.txt index 05968bd8..e320e781 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ httptools ujson; sys_platform != "win32" and implementation_name == "cpython" uvloop; sys_platform != "win32" and implementation_name == "cpython" websockets>=5.0,<6.0 +multidict>=4.0,<5.0 diff --git a/sanic/cookies.py b/sanic/cookies.py index f4cbf6a3..19daac11 100644 --- a/sanic/cookies.py +++ b/sanic/cookies.py @@ -47,16 +47,15 @@ class CookieJar(dict): super().__init__() self.headers = headers self.cookie_headers = {} + self.header_key = "Set-Cookie" def __setitem__(self, key, value): # If this cookie doesn't exist, add it to the header keys - cookie_header = self.cookie_headers.get(key) - if not cookie_header: + if not self.cookie_headers.get(key): cookie = Cookie(key, value) cookie['path'] = '/' - cookie_header = MultiHeader("Set-Cookie") - self.cookie_headers[key] = cookie_header - self.headers[cookie_header] = cookie + self.cookie_headers[key] = self.header_key + self.headers.add(self.header_key, cookie) return super().__setitem__(key, cookie) else: self[key].value = value @@ -67,7 +66,11 @@ class CookieJar(dict): self[key]['max-age'] = 0 else: cookie_header = self.cookie_headers[key] - del self.headers[cookie_header] + # remove it from header + cookies = self.headers.popall(cookie_header) + for cookie in cookies: + if cookie.key != key: + self.headers.add(cookie_header, cookie) del self.cookie_headers[key] return super().__delitem__(key) @@ -124,18 +127,3 @@ class Cookie(dict): output.append('%s=%s' % (self._keys[key], value)) return "; ".join(output).encode(encoding) - -# ------------------------------------------------------------ # -# Header Trickery -# ------------------------------------------------------------ # - - -class MultiHeader: - """String-holding object which allow us to set a header within response - that has a unique key, but may contain duplicate header names - """ - def __init__(self, name): - self.name = name - - def encode(self): - return self.name.encode() diff --git a/sanic/response.py b/sanic/response.py index b32e9daf..62daf91e 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -7,6 +7,7 @@ except BaseException: from json import dumps as json_dumps from aiofiles import open as open_async +from multidict import CIMultiDict from sanic import http from sanic.cookies import CookieJar @@ -53,7 +54,7 @@ class StreamingHTTPResponse(BaseHTTPResponse): self.content_type = content_type self.streaming_fn = streaming_fn self.status = status - self.headers = headers or {} + self.headers = CIMultiDict(headers or {}) self._cookies = None def write(self, data): @@ -124,7 +125,7 @@ class HTTPResponse(BaseHTTPResponse): self.body = body_bytes self.status = status - self.headers = headers or {} + self.headers = CIMultiDict(headers or {}) self._cookies = None def output( diff --git a/sanic/server.py b/sanic/server.py index fc8291b5..11e54edc 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -18,6 +18,7 @@ from time import time from httptools import HttpRequestParser from httptools.parser.errors import HttpParserError +from multidict import CIMultiDict try: import uvloop @@ -39,25 +40,6 @@ class Signal: stopped = False -class CIDict(dict): - """Case Insensitive dict where all keys are converted to lowercase - This does not maintain the inputted case when calling items() or keys() - in favor of speed, since headers are case insensitive - """ - - def get(self, key, default=None): - return super().get(key.casefold(), default) - - def __getitem__(self, key): - return super().__getitem__(key.casefold()) - - def __setitem__(self, key, value): - return super().__setitem__(key.casefold(), value) - - def __contains__(self, key): - return super().__contains__(key.casefold()) - - class HttpProtocol(asyncio.Protocol): __slots__ = ( # event loop, connection @@ -256,7 +238,7 @@ class HttpProtocol(asyncio.Protocol): def on_headers_complete(self): self.request = self.request_class( url_bytes=self.url, - headers=CIDict(self.headers), + headers=CIMultiDict(self.headers), version=self.parser.get_http_version(), method=self.parser.get_method().decode(), transport=self.transport diff --git a/setup.py b/setup.py index 73cb559f..34703ab4 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ requirements = [ ujson, 'aiofiles>=0.3.0', 'websockets>=5.0,<6.0', + 'multidict>=4.0,<5.0', ] if strtobool(os.environ.get("SANIC_NO_UJSON", "no")): print("Installing without uJSON") diff --git a/tests/test_cookies.py b/tests/test_cookies.py index 84b493cb..61f50735 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -25,6 +25,7 @@ def test_cookies(): assert response.text == 'Cookies are: working!' assert response_cookies['right_back'].value == 'at you' + @pytest.mark.parametrize("httponly,expected", [ (False, False), (True, True), diff --git a/tests/test_response.py b/tests/test_response.py index 12049460..36259970 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -64,6 +64,25 @@ def test_method_not_allowed(): assert response.headers['Content-Length'] == '0' +def test_response_header(): + app = Sanic('test_response_header') + @app.get('/') + async def test(request): + return json({ + "ok": True + }, headers={ + 'CONTENT-TYPE': 'application/json' + }) + + request, response = app.test_client.get('/') + assert dict(response.headers) == { + 'Connection': 'keep-alive', + 'Keep-Alive': '2', + 'Content-Length': '11', + 'Content-Type': 'application/json', + } + + @pytest.fixture def json_app(): app = Sanic('json') From cd22745e6ba488bf0e58b77eaf4db48438a8cb8d Mon Sep 17 00:00:00 2001 From: Ave Date: Fri, 13 Jul 2018 07:31:33 +0300 Subject: [PATCH 03/32] Sanitize the URL before redirecting (#1260) * URL Quote the URL before redirecting * Use safe url instead of unsafe one * Fix query params * fix build * Whitelist all reserved characters from rfc3986 * Add tests for redirect url sanitizing * Remove check for resulting URL on header injection test The thing the tests are testing for can be implemented in other ways that don't redirect to 100% the same address, but they'll all have to match the remaining parts of the test to succeed. --- sanic/response.py | 6 +++++- tests/test_redirect.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 62daf91e..2281766e 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,5 +1,6 @@ from mimetypes import guess_type from os import path +from urllib.parse import quote_plus try: from ujson import dumps as json_dumps @@ -360,8 +361,11 @@ def redirect(to, headers=None, status=302, """ headers = headers or {} + # URL Quote the URL before redirecting + safe_to = quote_plus(to, safe=":/#?&=@[]!$&'()*+,;") + # According to RFC 7231, a relative URI is now permitted. - headers['Location'] = to + headers['Location'] = safe_to return HTTPResponse( status=status, diff --git a/tests/test_redirect.py b/tests/test_redirect.py index f5b734e3..5fdec2a6 100644 --- a/tests/test_redirect.py +++ b/tests/test_redirect.py @@ -32,6 +32,10 @@ def redirect_app(): def handler(request): return text('OK') + @app.route('/redirect_with_header_injection') + async def redirect_with_header_injection(request): + return redirect("/unsafe\ntest-header: test-value\n\ntest-body") + return app @@ -92,3 +96,16 @@ def test_chained_redirect(redirect_app): assert response.url.endswith('/3') except AttributeError: assert response.url.path.endswith('/3') + + +def test_redirect_with_header_injection(redirect_app): + """ + Test redirection to a URL with header and body injections. + """ + request, response = redirect_app.test_client.get( + "/redirect_with_header_injection", + allow_redirects=False) + + assert response.status == 302 + assert "test-header" not in response.headers + assert not response.text.startswith('test-body') From a39a7ca9d52fe3914c7ce16569e85cad3765faec Mon Sep 17 00:00:00 2001 From: John Doe <34662+johndoe46@users.noreply.github.com> Date: Mon, 16 Jul 2018 21:13:27 +0200 Subject: [PATCH 04/32] Add url_bytes to Request (#1258) We need to have access to the raw unparsed URL. --- sanic/request.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/request.py b/sanic/request.py index 7ce7620d..c8b470d4 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -48,10 +48,11 @@ class Request(dict): 'app', 'headers', 'version', 'method', '_cookies', 'transport', 'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', '_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr', - '_socket', '_port', '__weakref__' + '_socket', '_port', '__weakref__', 'raw_url' ) def __init__(self, url_bytes, headers, version, method, transport): + self.raw_url = url_bytes # TODO: Content-Encoding detection self._parsed_url = parse_url(url_bytes) self.app = None From 599834b0e1c41d934dce30b6393926635f72b78e Mon Sep 17 00:00:00 2001 From: ciscorn Date: Tue, 17 Jul 2018 04:20:26 +0900 Subject: [PATCH 05/32] Add subprotocols param to add_websocket_route (#1261) --- sanic/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index ca50edc1..7af82111 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -315,13 +315,13 @@ class Sanic: return response def add_websocket_route(self, handler, uri, host=None, - strict_slashes=None, name=None): + strict_slashes=None, subprotocols=None, name=None): """A helper method to register a function as a websocket route.""" if strict_slashes is None: strict_slashes = self.strict_slashes return self.websocket(uri, host=host, strict_slashes=strict_slashes, - name=name)(handler) + subprotocols=subprotocols, name=name)(handler) def enable_websocket(self, enable=True): """Enable or disable the support for websocket. From 377c9890a3fbd322f5f4cc91633c972ef720bf35 Mon Sep 17 00:00:00 2001 From: Cosmo Borsky Date: Fri, 20 Jul 2018 16:39:10 -0400 Subject: [PATCH 06/32] Support status code for file reponse (#1269) Fixes #1268 --- sanic/response.py | 13 ++++++------- tests/test_response.py | 8 +++++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 2281766e..2d2f5b96 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -233,8 +233,8 @@ def html(body, status=200, headers=None): content_type="text/html; charset=utf-8") -async def file( - location, mime_type=None, headers=None, filename=None, _range=None): +async def file(location, status=200, mime_type=None, headers=None, + filename=None, _range=None): """Return a response object with file data. :param location: Location of file on system. @@ -260,15 +260,14 @@ async def file( out_stream = await _file.read() mime_type = mime_type or guess_type(filename)[0] or 'text/plain' - return HTTPResponse(status=200, + return HTTPResponse(status=status, headers=headers, content_type=mime_type, body_bytes=out_stream) -async def file_stream( - location, chunk_size=4096, mime_type=None, headers=None, - filename=None, _range=None): +async def file_stream(location, status=200, chunk_size=4096, mime_type=None, + headers=None, filename=None, _range=None): """Return a streaming response object with file data. :param location: Location of file on system. @@ -315,7 +314,7 @@ async def file_stream( headers['Content-Range'] = 'bytes %s-%s/%s' % ( _range.start, _range.end, _range.total) return StreamingHTTPResponse(streaming_fn=_streaming_fn, - status=200, + status=status, headers=headers, content_type=mime_type) diff --git a/tests/test_response.py b/tests/test_response.py index 36259970..96c06ce6 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -227,17 +227,19 @@ def get_file_content(static_file_directory, file_name): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) -def test_file_response(file_name, static_file_directory): +@pytest.mark.parametrize('status', [200, 401]) +def test_file_response(file_name, static_file_directory, status): app = Sanic('test_file_helper') @app.route('/files/', methods=['GET']) def file_route(request, filename): file_path = os.path.join(static_file_directory, filename) file_path = os.path.abspath(unquote(file_path)) - return file(file_path, mime_type=guess_type(file_path)[0] or 'text/plain') + return file(file_path, status=status, + mime_type=guess_type(file_path)[0] or 'text/plain') request, response = app.test_client.get('/files/{}'.format(file_name)) - assert response.status == 200 + assert response.status == status assert response.body == get_file_content(static_file_directory, file_name) assert 'Content-Disposition' not in response.headers From b238be54a4d13e37954e025e76472c30029390af Mon Sep 17 00:00:00 2001 From: Cosmo Borsky Date: Sat, 21 Jul 2018 01:31:15 -0400 Subject: [PATCH 07/32] Add content_type flag to Sanic.static (#1267) * Add content_type flag to Sanic.static Fixes #1266 * Fix flake8 error in travis Add line to document `content_type` arg * Fix content_type for file streams Update tests herp derp * Remove content_type as an arg to HTTPResponse `response.HTTPResponse` will default to `headers['Content-Type']` instead of `content_type` https://github.com/channelcat/sanic/pull/1267#discussion_r204190913 --- sanic/app.py | 5 ++-- sanic/static.py | 9 ++++---- tests/static/test.html | 26 +++++++++++++++++++++ tests/test_blueprints.py | 49 ++++++++++++++++++++++++++++++++-------- tests/test_static.py | 15 ++++++++++++ 5 files changed, 89 insertions(+), 15 deletions(-) create mode 100644 tests/static/test.html diff --git a/sanic/app.py b/sanic/app.py index 7af82111..f9eda2d4 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -386,13 +386,14 @@ class Sanic: def static(self, uri, file_or_directory, pattern=r'/?.+', use_modified_since=True, use_content_range=False, stream_large_files=False, name='static', host=None, - strict_slashes=None): + strict_slashes=None, content_type=None): """Register a root to serve files from. The input can either be a file or a directory. See """ static_register(self, uri, file_or_directory, pattern, use_modified_since, use_content_range, - stream_large_files, name, host, strict_slashes) + stream_large_files, name, host, strict_slashes, + content_type) def blueprint(self, blueprint, **options): """Register a blueprint on the application. diff --git a/sanic/static.py b/sanic/static.py index f2d02ab0..07831390 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -19,7 +19,7 @@ from sanic.response import file, file_stream, HTTPResponse def register(app, uri, file_or_directory, pattern, use_modified_since, use_content_range, stream_large_files, name='static', host=None, - strict_slashes=None): + strict_slashes=None, content_type=None): # TODO: Though sanic is not a file server, I feel like we should at least # make a good effort here. Modified-since is nice, but we could # also look into etags, expires, and caching @@ -41,6 +41,7 @@ def register(app, uri, file_or_directory, pattern, If this is an integer, this represents the threshold size to switch to file_stream() :param name: user defined name used for url_for + :param content_type: user defined content type for header """ # If we're not trying to match a file directly, # serve from the folder @@ -95,10 +96,10 @@ def register(app, uri, file_or_directory, pattern, del headers['Content-Length'] for key, value in _range.headers.items(): headers[key] = value + headers['Content-Type'] = content_type \ + or guess_type(file_path)[0] or 'text/plain' if request.method == 'HEAD': - return HTTPResponse( - headers=headers, - content_type=guess_type(file_path)[0] or 'text/plain') + return HTTPResponse(headers=headers) else: if stream_large_files: if isinstance(stream_large_files, int): diff --git a/tests/static/test.html b/tests/static/test.html new file mode 100644 index 00000000..4ba71873 --- /dev/null +++ b/tests/static/test.html @@ -0,0 +1,26 @@ + + +
+                 ▄▄▄▄▄
+        ▀▀▀██████▄▄▄       _______________
+      ▄▄▄▄▄  █████████▄  /                 \
+     ▀▀▀▀█████▌ ▀▐▄ ▀▐█ |   Gotta go fast!  |
+   ▀▀█████▄▄ ▀██████▄██ | _________________/
+   ▀▄▄▄▄▄  ▀▀█▄▀█════█▀ |/
+        ▀▀▀▄  ▀▀███ ▀       ▄▄
+     ▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
+   ██▀▄▄▄██▀▄███▀ ▀▀████      ▄██
+▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███     ▌▄▄▀
+▌    ▐▀████▐███▒▒▒▒▒▐██▌
+▀▄▄▄▄▀   ▀▀████▒▒▒▒▄██▀
+          ▀▀█████████▀
+        ▄▄██▀██████▀█
+      ▄██▀     ▀▀▀  █
+     ▄█             ▐▌
+ ▄▄▄▄█▌              ▀█▄▄▄▄▀▀▄
+▌     ▐                ▀▀▄▄▄▀
+ ▀▀▄▄▀
+
+
+ + diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 4c321646..37756085 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -1,5 +1,6 @@ import asyncio import inspect +import os import pytest from sanic import Sanic @@ -13,6 +14,14 @@ from sanic.constants import HTTP_METHODS # GET # ------------------------------------------------------------ # +def get_file_path(static_file_directory, file_name): + return os.path.join(static_file_directory, file_name) + +def get_file_content(static_file_directory, file_name): + """The content of the static file to check""" + with open(get_file_path(static_file_directory, file_name), 'rb') as file: + return file.read() + @pytest.mark.parametrize('method', HTTP_METHODS) def test_versioned_routes_get(method): app = Sanic('test_shorhand_routes_get') @@ -348,6 +357,28 @@ def test_bp_static(): assert response.status == 200 assert response.body == current_file_contents +@pytest.mark.parametrize('file_name', ['test.html']) +def test_bp_static_content_type(file_name): + # This is done here, since no other test loads a file here + current_file = inspect.getfile(inspect.currentframe()) + current_directory = os.path.dirname(os.path.abspath(current_file)) + static_directory = os.path.join(current_directory, 'static') + + app = Sanic('test_static') + blueprint = Blueprint('test_static') + blueprint.static( + '/testing.file', + get_file_path(static_directory, file_name), + content_type='text/html; charset=utf-8' + ) + + app.blueprint(blueprint) + + request, response = app.test_client.get('/testing.file') + assert response.status == 200 + assert response.body == get_file_content(static_directory, file_name) + assert response.headers['Content-Type'] == 'text/html; charset=utf-8' + def test_bp_shorthand(): app = Sanic('test_shorhand_routes') blueprint = Blueprint('test_shorhand_routes') @@ -449,41 +480,41 @@ def test_bp_shorthand(): def test_bp_group(): app = Sanic('test_nested_bp_groups') - + deep_0 = Blueprint('deep_0', url_prefix='/deep') deep_1 = Blueprint('deep_1', url_prefix = '/deep1') @deep_0.route('/') def handler(request): return text('D0_OK') - + @deep_1.route('/bottom') def handler(request): return text('D1B_OK') mid_0 = Blueprint.group(deep_0, deep_1, url_prefix='/mid') mid_1 = Blueprint('mid_tier', url_prefix='/mid1') - + @mid_1.route('/') def handler(request): return text('M1_OK') top = Blueprint.group(mid_0, mid_1) - + app.blueprint(top) - + @app.route('/') def handler(request): return text('TOP_OK') - + request, response = app.test_client.get('/') assert response.text == 'TOP_OK' - + request, response = app.test_client.get('/mid1') assert response.text == 'M1_OK' - + request, response = app.test_client.get('/mid/deep') assert response.text == 'D0_OK' - + request, response = app.test_client.get('/mid/deep1/bottom') assert response.text == 'D1B_OK' diff --git a/tests/test_static.py b/tests/test_static.py index 276001cc..3335e248 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -36,6 +36,21 @@ def test_static_file(static_file_directory, file_name): assert response.body == get_file_content(static_file_directory, file_name) +@pytest.mark.parametrize('file_name', ['test.html']) +def test_static_file_content_type(static_file_directory, file_name): + app = Sanic('test_static') + app.static( + '/testing.file', + get_file_path(static_file_directory, file_name), + content_type='text/html; charset=utf-8' + ) + + request, response = app.test_client.get('/testing.file') + assert response.status == 200 + assert response.body == get_file_content(static_file_directory, file_name) + assert response.headers['Content-Type'] == 'text/html; charset=utf-8' + + @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) @pytest.mark.parametrize('base_uri', ['/static', '', '/dir']) def test_static_directory(file_name, base_uri, static_file_directory): From 39ff02b6e4b2f3af9eafa52a2e9ba5d4fd5009e5 Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Mon, 6 Aug 2018 14:12:30 +1000 Subject: [PATCH 08/32] Modifications the `handle_request` function to detect and gracefully handle the case that the request_handler Task is canceled by the sanic server while it is handling the request. One common occurrence of this is when the server issues a ResponseTimeout error, it also cancels the response_handler Task. The Canceled exception handler purposely sets `response` to `None` to drop references to the handler coroutine, in an attempt to preemptively release resources. This commit also fixes a possible reference-before-assignment of the `response` variable in the `handle_request` function. Finally, another byproduct of this change is that ResponseMiddleware will no longer run if the `response` is `None`. --- sanic/app.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index f9eda2d4..e4e4ff20 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -571,6 +571,10 @@ class Sanic: :return: Nothing """ + # Define `response` var here to remove warnings about + # allocation before assignment below. + response = None + cancelled = False try: # -------------------------------------------- # # Request Middleware @@ -597,6 +601,13 @@ class Sanic: response = handler(request, *args, **kwargs) if isawaitable(response): response = await response + except CancelledError: + # If response handler times out, the server handles the error + # and cancels the handle_request job. + # In this case, the transport is already closed and we cannot + # issue a response. + response = None + cancelled = True except Exception as e: # -------------------------------------------- # # Response Generation Failed @@ -622,13 +633,22 @@ class Sanic: # -------------------------------------------- # # Response Middleware # -------------------------------------------- # - try: - response = await self._run_response_middleware(request, - response) - except BaseException: - error_logger.exception( - 'Exception occurred in one of response middleware handlers' - ) + # Don't run response middleware if response is None + if response is not None: + try: + response = await self._run_response_middleware(request, + response) + except CancelledError: + # Response middleware can timeout too, as above. + response = None + cancelled = True + except BaseException: + error_logger.exception( + 'Exception occurred in one of response ' + 'middleware handlers' + ) + if cancelled: + raise CancelledError() # pass the response to the correct callback if isinstance(response, StreamingHTTPResponse): From afea15e4a7834a83ff95ddf974852272e4c94b80 Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Mon, 6 Aug 2018 15:02:12 +1000 Subject: [PATCH 09/32] Add a test for the graceful CancelledError handling. The user app should _never_ see a CancelledError bubble up, nor should they be able to catch it, because the response is already sent at that point. --- tests/test_response_timeout.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_response_timeout.py b/tests/test_response_timeout.py index bf55a42e..44204b5e 100644 --- a/tests/test_response_timeout.py +++ b/tests/test_response_timeout.py @@ -7,6 +7,7 @@ from sanic.config import Config Config.RESPONSE_TIMEOUT = 1 response_timeout_app = Sanic('test_response_timeout') response_timeout_default_app = Sanic('test_response_timeout_default') +response_handler_cancelled_app = Sanic('test_response_handler_cancelled') @response_timeout_app.route('/1') @@ -36,3 +37,29 @@ def test_default_server_error_response_timeout(): request, response = response_timeout_default_app.test_client.get('/1') assert response.status == 503 assert response.text == 'Error: Response Timeout' + + +response_handler_cancelled_app.flag = False + + +@response_handler_cancelled_app.exception(asyncio.CancelledError) +def handler_cancelled(request, exception): + # If we get a CancelledError, it means sanic has already sent a response, + # we should not ever have to handle a CancelledError. + response_handler_cancelled_app.flag = True + return text("App received CancelledError!", 500) + # The client will never receive this response, because the socket + # is already closed when we get a CancelledError. + + +@response_handler_cancelled_app.route('/1') +async def handler_3(request): + await asyncio.sleep(2) + return text('OK') + + +def test_response_handler_cancelled(): + request, response = response_handler_cancelled_app.test_client.get('/1') + assert response.status == 503 + assert response.text == 'Error: Response Timeout' + assert response_handler_cancelled_app.flag is False From 212da1029e9a0ed786b5a1bfaf10183bfef9a790 Mon Sep 17 00:00:00 2001 From: abuckenheimer Date: Tue, 7 Aug 2018 14:48:18 -0400 Subject: [PATCH 10/32] disabled auto_reload by default in windows (#1280) --- sanic/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index f9eda2d4..e82028f4 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -671,8 +671,8 @@ class Sanic: """ # Default auto_reload to false auto_reload = False - # If debug is set, default it to true - if debug: + # If debug is set, default it to true (unless on windows) + if debug and os.name == 'posix': auto_reload = True # Allow for overriding either of the defaults auto_reload = kwargs.get("auto_reload", auto_reload) From 6abdf9f9c162140a4981c21361d334323ed47d65 Mon Sep 17 00:00:00 2001 From: hqy Date: Thu, 16 Aug 2018 01:23:04 +0800 Subject: [PATCH 11/32] fixed #1143 (#1276) * fixed #1143 * fixed build failed with create_serve call _helper failed --- sanic/app.py | 16 ++++++++-------- sanic/config.py | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index e82028f4..367acc5a 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -688,11 +688,12 @@ class Sanic: warnings.simplefilter('default') warnings.warn("stop_event will be removed from future versions.", DeprecationWarning) + # compatibility old access_log params + self.config.ACCESS_LOG = access_log server_settings = self._helper( host=host, port=port, debug=debug, ssl=ssl, sock=sock, workers=workers, protocol=protocol, backlog=backlog, - register_sys_signals=register_sys_signals, - access_log=access_log, auto_reload=auto_reload) + register_sys_signals=register_sys_signals, auto_reload=auto_reload) try: self.is_running = True @@ -746,12 +747,12 @@ class Sanic: warnings.simplefilter('default') warnings.warn("stop_event will be removed from future versions.", DeprecationWarning) - + # compatibility old access_log params + self.config.ACCESS_LOG = access_log server_settings = self._helper( host=host, port=port, debug=debug, ssl=ssl, sock=sock, loop=get_event_loop(), protocol=protocol, - backlog=backlog, run_async=True, - access_log=access_log) + backlog=backlog, run_async=True) # Trigger before_start events await self.trigger_events( @@ -796,8 +797,7 @@ class Sanic: def _helper(self, host=None, port=None, debug=False, ssl=None, sock=None, workers=1, loop=None, protocol=HttpProtocol, backlog=100, stop_event=None, - register_sys_signals=True, run_async=False, access_log=True, - auto_reload=False): + register_sys_signals=True, run_async=False, auto_reload=False): """Helper function used by `run` and `create_server`.""" if isinstance(ssl, dict): # try common aliaseses @@ -838,7 +838,7 @@ class Sanic: 'loop': loop, 'register_sys_signals': register_sys_signals, 'backlog': backlog, - 'access_log': access_log, + 'access_log': self.config.ACCESS_LOG, 'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE, 'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE, 'websocket_read_limit': self.config.WEBSOCKET_READ_LIMIT, diff --git a/sanic/config.py b/sanic/config.py index 8e1f383c..c5e42de5 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -39,6 +39,7 @@ class Config(dict): self.WEBSOCKET_READ_LIMIT = 2 ** 16 self.WEBSOCKET_WRITE_LIMIT = 2 ** 16 self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec + self.ACCESS_LOG = True if load_env: prefix = SANIC_PREFIX if load_env is True else load_env From ec226e33cb3a75af3407fca56dbf0aa4b7078db3 Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Thu, 16 Aug 2018 15:00:23 +1000 Subject: [PATCH 12/32] Pin aiohttp <= 3.2.1 in requirements-dev.txt (fixes errors for new contributors checking out the code and setting up a dev environment) Future-proof the some test cases so they work with aiohttp >= 3.3.0, in case we bump the aiohttp version in the future. --- requirements-dev.txt | 2 +- tests/test_keep_alive_timeout.py | 30 +++++++++++-- tests/test_request_timeout.py | 73 ++++++++++++++++++++++++++------ 3 files changed, 88 insertions(+), 17 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 3d94c51d..004f6f9e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ aiofiles -aiohttp>=2.3.0 +aiohttp>=2.3.0,<=3.2.1 chardet<=2.3.0 beautifulsoup4 coverage diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index 2a9e93a2..53a2872e 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -9,14 +9,39 @@ import aiohttp from aiohttp import TCPConnector from sanic.testing import SanicTestClient, HOST, PORT +try: + try: + import packaging # direct use + except ImportError: + # setuptools v39.0 and above. + try: + from setuptools.extern import packaging + except ImportError: + # Before setuptools v39.0 + from pkg_resources.extern import packaging + version = packaging.version +except ImportError: + raise RuntimeError("The 'packaging' library is missing.") + +aiohttp_version = version.parse(aiohttp.__version__) class ReuseableTCPConnector(TCPConnector): def __init__(self, *args, **kwargs): super(ReuseableTCPConnector, self).__init__(*args, **kwargs) self.old_proto = None - if aiohttp.__version__ >= '3.0': - + if aiohttp_version >= version.parse('3.3.0'): + async def connect(self, req, traces, timeout): + new_conn = await super(ReuseableTCPConnector, self)\ + .connect(req, traces, timeout) + if self.old_proto is not None: + if self.old_proto != new_conn._protocol: + raise RuntimeError( + "We got a new connection, wanted the same one!") + print(new_conn.__dict__) + self.old_proto = new_conn._protocol + return new_conn + elif aiohttp_version >= version.parse('3.0.0'): async def connect(self, req, traces=None): new_conn = await super(ReuseableTCPConnector, self)\ .connect(req, traces=traces) @@ -28,7 +53,6 @@ class ReuseableTCPConnector(TCPConnector): self.old_proto = new_conn._protocol return new_conn else: - async def connect(self, req): new_conn = await super(ReuseableTCPConnector, self)\ .connect(req) diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index b3eb78aa..672d0588 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -5,9 +5,24 @@ import asyncio from sanic.response import text from sanic.config import Config import aiohttp -from aiohttp import TCPConnector +from aiohttp import TCPConnector, ClientResponse from sanic.testing import SanicTestClient, HOST, PORT +try: + try: + import packaging # direct use + except ImportError: + # setuptools v39.0 and above. + try: + from setuptools.extern import packaging + except ImportError: + # Before setuptools v39.0 + from pkg_resources.extern import packaging + version = packaging.version +except ImportError: + raise RuntimeError("The 'packaging' library is missing.") + +aiohttp_version = version.parse(aiohttp.__version__) class DelayableTCPConnector(TCPConnector): @@ -38,8 +53,11 @@ class DelayableTCPConnector(TCPConnector): self.orig_start = getattr(resp, 'start') try: - ret = await self.orig_start(connection, - read_until_eof) + if aiohttp_version >= version.parse("3.3.0"): + ret = await self.orig_start(connection) + else: + ret = await self.orig_start(connection, + read_until_eof) except Exception as e: raise e return ret @@ -57,15 +75,31 @@ class DelayableTCPConnector(TCPConnector): await asyncio.sleep(self.delay) t = req.loop.time() print("sending at {}".format(t), flush=True) - conn = next(iter(args)) # first arg is connection - if aiohttp.__version__ >= "3.1.0": + conn = next(iter(args)) # first arg is connection + + if aiohttp_version >= version.parse("3.1.0"): try: delayed_resp = await self.orig_send(*args, **kwargs) except Exception as e: - return aiohttp.ClientResponse(req.method, req.url, - writer=None, continue100=None, timer=None, - request_info=None, auto_decompress=None, traces=[], - loop=req.loop, session=None) + if aiohttp_version >= version.parse("3.3.0"): + return aiohttp.ClientResponse(req.method, req.url, + writer=None, + continue100=None, + timer=None, + request_info=None, + traces=[], + loop=req.loop, + session=None) + else: + return aiohttp.ClientResponse(req.method, req.url, + writer=None, + continue100=None, + timer=None, + request_info=None, + auto_decompress=None, + traces=[], + loop=req.loop, + session=None) else: try: delayed_resp = self.orig_send(*args, **kwargs) @@ -73,7 +107,7 @@ class DelayableTCPConnector(TCPConnector): return aiohttp.ClientResponse(req.method, req.url) return delayed_resp - if aiohttp.__version__ >= "3.1.0": + if aiohttp_version >= version.parse("3.1.0"): # aiohttp changed the request.send method to async async def send(self, *args, **kwargs): gen = self.delayed_send(*args, **kwargs) @@ -96,12 +130,25 @@ class DelayableTCPConnector(TCPConnector): self._post_connect_delay = _post_connect_delay self._pre_request_delay = _pre_request_delay - if aiohttp.__version__ >= '3.0': - + if aiohttp_version >= version.parse("3.3.0"): + async def connect(self, req, traces, timeout): + d_req = DelayableTCPConnector.\ + RequestContextManager(req, self._pre_request_delay) + conn = await super(DelayableTCPConnector, self).\ + connect(req, traces, timeout) + if self._post_connect_delay and self._post_connect_delay > 0: + await asyncio.sleep(self._post_connect_delay, + loop=self._loop) + req.send = d_req.send + t = req.loop.time() + print("Connected at {}".format(t), flush=True) + return conn + elif aiohttp_version >= version.parse("3.0.0"): async def connect(self, req, traces=None): d_req = DelayableTCPConnector.\ RequestContextManager(req, self._pre_request_delay) - conn = await super(DelayableTCPConnector, self).connect(req, traces=traces) + conn = await super(DelayableTCPConnector, self).\ + connect(req, traces=traces) if self._post_connect_delay and self._post_connect_delay > 0: await asyncio.sleep(self._post_connect_delay, loop=self._loop) From 1814ff05f4fde9f875aa7f55b27b59b9a0fcb798 Mon Sep 17 00:00:00 2001 From: Innokenty Lebedev Date: Thu, 16 Aug 2018 21:59:58 +0300 Subject: [PATCH 13/32] Add sse extension (#1288) --- docs/sanic/extensions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index 01c89f95..c0728627 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -31,3 +31,4 @@ A list of Sanic extensions created by the community. - [Sanic-Auth](https://github.com/pyx/sanic-auth): A minimal backend agnostic session-based user authentication mechanism for Sanic. - [Sanic-CookieSession](https://github.com/pyx/sanic-cookiesession): A client-side only, cookie-based session, similar to the built-in session in Flask. - [Sanic-WTF](https://github.com/pyx/sanic-wtf): Sanic-WTF makes using WTForms with Sanic and CSRF (Cross-Site Request Forgery) protection a little bit easier. +- [sanic-sse](https://github.com/inn0kenty/sanic_sse): [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) implementation for Sanic. From 79e35bbdf600d62707b6d3a5d57051e3c608daf2 Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Fri, 17 Aug 2018 16:30:03 +1000 Subject: [PATCH 14/32] Fix auto_reload in Linux (#1286) * Fix two problems with the auto_reloader in Linux. 1) Change 'posix' to 'linux' in sys.plaform check, because 'posix' is an invalid value and 'linux' is the correct value to use here. 2) In kill_process_children, don't just kill the 2nd level procs, also kill the 1st level procs. Also in kill_process_children, catch and ignore errors in the case that the child proc is already killed. * Fix flake8 formatting on PR --- sanic/reloader_helpers.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/sanic/reloader_helpers.py b/sanic/reloader_helpers.py index 73759124..e0cb42e0 100644 --- a/sanic/reloader_helpers.py +++ b/sanic/reloader_helpers.py @@ -74,7 +74,14 @@ def kill_process_children_unix(pid): with open(children_proc_path) as children_list_file_2: children_list_pid_2 = children_list_file_2.read().split() for _pid in children_list_pid_2: - os.kill(int(_pid), signal.SIGTERM) + try: + os.kill(int(_pid), signal.SIGTERM) + except ProcessLookupError: + continue + try: + os.kill(int(child_pid), signal.SIGTERM) + except ProcessLookupError: + continue def kill_process_children_osx(pid): @@ -94,7 +101,7 @@ def kill_process_children(pid): """ if sys.platform == 'darwin': kill_process_children_osx(pid) - elif sys.platform == 'posix': + elif sys.platform == 'linux': kill_process_children_unix(pid) else: pass # should signal error here @@ -136,8 +143,8 @@ def watchdog(sleep_interval): continue elif mtime > old_time: kill_process_children(worker_process.pid) + worker_process.terminate() worker_process = restart_with_reloader() - mtimes[filename] = mtime break From b398c1fe72977583e1c663b7a4c07044e6f272f1 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 17 Aug 2018 11:43:15 -0700 Subject: [PATCH 15/32] Increment to 0.8.0 Signed-off-by: Eli Uriegas --- sanic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index 78bc7bd9..5e6ff4da 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from sanic.app import Sanic from sanic.blueprints import Blueprint -__version__ = '0.7.0' +__version__ = '0.8.0' __all__ = ['Sanic', 'Blueprint'] From 30e6a310f132752669a74927530e8bc52a51e98e Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Sun, 19 Aug 2018 11:12:13 +1000 Subject: [PATCH 16/32] Pausable response streams (#1179) * This commit adds handlers for the asyncio/uvloop protocol callbacks for pause_writing and resume_writing. These are needed for the correct functioning of built-in tcp flow-control provided by uvloop and asyncio. This is somewhat of a breaking change, because the `write` function in user streaming callbacks now must be `await`ed. This is necessary because it is possible now that the http protocol may be paused, and any calls to write may need to wait on an async event to be called to become unpaused. Updated examples and tests to reflect this change. This change does not apply to websocket connections. A change to websocket connections may be required to match this change. * Fix a couple of PEP8 errors caused by previous rebase. * update docs add await syntax to response.write in response-streaming docs. * remove commented out code from a test file --- docs/sanic/streaming.md | 8 ++++---- examples/request_stream/server.py | 2 +- sanic/response.py | 19 +++++++++++-------- sanic/server.py | 20 ++++++++++++++++++-- tests/test_request_stream.py | 16 ++++++++-------- tests/test_response.py | 24 ++++++++++++++++++------ 6 files changed, 60 insertions(+), 29 deletions(-) diff --git a/docs/sanic/streaming.md b/docs/sanic/streaming.md index a785322a..bf3ca664 100644 --- a/docs/sanic/streaming.md +++ b/docs/sanic/streaming.md @@ -37,7 +37,7 @@ async def handler(request): if body is None: break body = body.decode('utf-8').replace('1', 'A') - response.write(body) + await response.write(body) return stream(streaming) @@ -85,8 +85,8 @@ app = Sanic(__name__) @app.route("/") async def test(request): async def sample_streaming_fn(response): - response.write('foo,') - response.write('bar') + await response.write('foo,') + await response.write('bar') return stream(sample_streaming_fn, content_type='text/csv') ``` @@ -100,7 +100,7 @@ async def index(request): conn = await asyncpg.connect(database='test') async with conn.transaction(): async for record in conn.cursor('SELECT generate_series(0, 10)'): - response.write(record[0]) + await response.write(record[0]) return stream(stream_from_db) ``` diff --git a/examples/request_stream/server.py b/examples/request_stream/server.py index e53a224c..d3d35aef 100644 --- a/examples/request_stream/server.py +++ b/examples/request_stream/server.py @@ -30,7 +30,7 @@ async def handler(request): if body is None: break body = body.decode('utf-8').replace('1', 'A') - response.write(body) + await response.write(body) return stream(streaming) diff --git a/sanic/response.py b/sanic/response.py index 2d2f5b96..f169b4f2 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -46,7 +46,7 @@ class BaseHTTPResponse: class StreamingHTTPResponse(BaseHTTPResponse): __slots__ = ( - 'transport', 'streaming_fn', 'status', + 'protocol', 'streaming_fn', 'status', 'content_type', 'headers', '_cookies' ) @@ -58,7 +58,7 @@ class StreamingHTTPResponse(BaseHTTPResponse): self.headers = CIMultiDict(headers or {}) self._cookies = None - def write(self, data): + async def write(self, data): """Writes a chunk of data to the streaming response. :param data: bytes-ish data to be written. @@ -66,8 +66,9 @@ class StreamingHTTPResponse(BaseHTTPResponse): if type(data) != bytes: data = self._encode_body(data) - self.transport.write( + self.protocol.push_data( b"%x\r\n%b\r\n" % (len(data), data)) + await self.protocol.drain() async def stream( self, version="1.1", keep_alive=False, keep_alive_timeout=None): @@ -77,10 +78,12 @@ class StreamingHTTPResponse(BaseHTTPResponse): headers = self.get_headers( version, keep_alive=keep_alive, keep_alive_timeout=keep_alive_timeout) - self.transport.write(headers) - + self.protocol.push_data(headers) + await self.protocol.drain() await self.streaming_fn(self) - self.transport.write(b'0\r\n\r\n') + self.protocol.push_data(b'0\r\n\r\n') + # no need to await drain here after this write, because it is the + # very last thing we write and nothing needs to wait for it. def get_headers( self, version="1.1", keep_alive=False, keep_alive_timeout=None): @@ -298,13 +301,13 @@ async def file_stream(location, status=200, chunk_size=4096, mime_type=None, if len(content) < 1: break to_send -= len(content) - response.write(content) + await response.write(content) else: while True: content = await _file.read(chunk_size) if len(content) < 1: break - response.write(content) + await response.write(content) finally: await _file.close() return # Returning from this fn closes the stream diff --git a/sanic/server.py b/sanic/server.py index 11e54edc..d5a8f211 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -55,7 +55,8 @@ class HttpProtocol(asyncio.Protocol): # connection management '_total_request_size', '_request_timeout_handler', '_response_timeout_handler', '_keep_alive_timeout_handler', - '_last_request_time', '_last_response_time', '_is_stream_handler') + '_last_request_time', '_last_response_time', '_is_stream_handler', + '_not_paused') def __init__(self, *, loop, request_handler, error_handler, signal=Signal(), connections=set(), request_timeout=60, @@ -82,6 +83,7 @@ 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) self._total_request_size = 0 self._request_timeout_handler = None self._response_timeout_handler = None @@ -96,6 +98,7 @@ class HttpProtocol(asyncio.Protocol): if 'requests_count' not in self.state: self.state['requests_count'] = 0 self._debug = debug + self._not_paused.set() @property def keep_alive(self): @@ -124,6 +127,12 @@ class HttpProtocol(asyncio.Protocol): if self._keep_alive_timeout_handler: self._keep_alive_timeout_handler.cancel() + def pause_writing(self): + self._not_paused.clear() + + def resume_writing(self): + self._not_paused.set() + def request_timeout_callback(self): # See the docstring in the RequestTimeout exception, to see # exactly what this timeout is checking for. @@ -351,6 +360,12 @@ class HttpProtocol(asyncio.Protocol): self._last_response_time = current_time self.cleanup() + async def drain(self): + await self._not_paused.wait() + + def push_data(self, data): + self.transport.write(data) + async def stream_response(self, response): """ Streams a response to the client asynchronously. Attaches @@ -360,9 +375,10 @@ class HttpProtocol(asyncio.Protocol): if self._response_timeout_handler: self._response_timeout_handler.cancel() self._response_timeout_handler = None + try: keep_alive = self.keep_alive - response.transport = self.transport + response.protocol = self await response.stream( self.request.version, keep_alive, self.keep_alive_timeout) self.log_response(response) diff --git a/tests/test_request_stream.py b/tests/test_request_stream.py index 4ca4e44e..b14aa519 100644 --- a/tests/test_request_stream.py +++ b/tests/test_request_stream.py @@ -83,7 +83,7 @@ def test_request_stream_app(): body = await request.stream.get() if body is None: break - response.write(body.decode('utf-8')) + await response.write(body.decode('utf-8')) return stream(streaming) @app.put('/_put') @@ -100,7 +100,7 @@ def test_request_stream_app(): body = await request.stream.get() if body is None: break - response.write(body.decode('utf-8')) + await response.write(body.decode('utf-8')) return stream(streaming) @app.patch('/_patch') @@ -117,7 +117,7 @@ def test_request_stream_app(): body = await request.stream.get() if body is None: break - response.write(body.decode('utf-8')) + await response.write(body.decode('utf-8')) return stream(streaming) assert app.is_request_stream is True @@ -177,7 +177,7 @@ def test_request_stream_handle_exception(): body = await request.stream.get() if body is None: break - response.write(body.decode('utf-8')) + await response.write(body.decode('utf-8')) return stream(streaming) # 404 @@ -231,7 +231,7 @@ def test_request_stream_blueprint(): body = await request.stream.get() if body is None: break - response.write(body.decode('utf-8')) + await response.write(body.decode('utf-8')) return stream(streaming) @bp.put('/_put') @@ -248,7 +248,7 @@ def test_request_stream_blueprint(): body = await request.stream.get() if body is None: break - response.write(body.decode('utf-8')) + await response.write(body.decode('utf-8')) return stream(streaming) @bp.patch('/_patch') @@ -265,7 +265,7 @@ def test_request_stream_blueprint(): body = await request.stream.get() if body is None: break - response.write(body.decode('utf-8')) + await response.write(body.decode('utf-8')) return stream(streaming) app.blueprint(bp) @@ -380,7 +380,7 @@ def test_request_stream(): body = await request.stream.get() if body is None: break - response.write(body.decode('utf-8')) + await response.write(body.decode('utf-8')) return stream(streaming) @app.get('/get') diff --git a/tests/test_response.py b/tests/test_response.py index 96c06ce6..6dcd2ea6 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -10,6 +10,7 @@ from random import choice from sanic import Sanic from sanic.response import HTTPResponse, stream, StreamingHTTPResponse, file, file_stream, json +from sanic.server import HttpProtocol from sanic.testing import HOST, PORT from unittest.mock import MagicMock @@ -30,9 +31,10 @@ def test_response_body_not_a_string(): async def sample_streaming_fn(response): - response.write('foo,') + await response.write('foo,') await asyncio.sleep(.001) - response.write('bar') + await response.write('bar') + def test_method_not_allowed(): @@ -189,20 +191,30 @@ def test_stream_response_includes_chunked_header(): def test_stream_response_writes_correct_content_to_transport(streaming_app): response = StreamingHTTPResponse(sample_streaming_fn) - response.transport = MagicMock(asyncio.Transport) + response.protocol = MagicMock(HttpProtocol) + response.protocol.transport = MagicMock(asyncio.Transport) + + async def mock_drain(): + pass + + def mock_push_data(data): + response.protocol.transport.write(data) + + response.protocol.push_data = mock_push_data + response.protocol.drain = mock_drain @streaming_app.listener('after_server_start') async def run_stream(app, loop): await response.stream() - assert response.transport.write.call_args_list[1][0][0] == ( + assert response.protocol.transport.write.call_args_list[1][0][0] == ( b'4\r\nfoo,\r\n' ) - assert response.transport.write.call_args_list[2][0][0] == ( + assert response.protocol.transport.write.call_args_list[2][0][0] == ( b'3\r\nbar\r\n' ) - assert response.transport.write.call_args_list[3][0][0] == ( + assert response.protocol.transport.write.call_args_list[3][0][0] == ( b'0\r\n\r\n' ) From fec81ffe732cdc842326a804f95f2e53c764d780 Mon Sep 17 00:00:00 2001 From: "dmitry.dygalo" Date: Sun, 26 Aug 2018 16:43:14 +0200 Subject: [PATCH 17/32] Reuse app fixture in tests --- tests/conftest.py | 8 ++ tests/test_bad_request.py | 4 +- tests/test_blueprints.py | 55 ++++-------- tests/test_config.py | 21 ++--- tests/test_cookies.py | 18 ++-- tests/test_create_task.py | 9 +- tests/test_custom_protocol.py | 11 +-- tests/test_dynamic_routes.py | 6 +- tests/test_exceptions.py | 3 +- tests/test_logging.py | 6 +- tests/test_middleware.py | 26 ++---- tests/test_multiprocessing.py | 4 +- tests/test_named_routes.py | 63 +++++--------- tests/test_payload_too_large.py | 30 +++---- tests/test_redirect.py | 4 +- tests/test_request_data.py | 7 +- tests/test_request_stream.py | 25 ++---- tests/test_requests.py | 65 +++++--------- tests/test_response.py | 36 +++----- tests/test_routes.py | 148 ++++++++++---------------------- tests/test_server_events.py | 44 ++++------ tests/test_signal_handlers.py | 7 +- tests/test_static.py | 33 +++---- tests/test_url_building.py | 35 +++----- tests/test_url_for_static.py | 27 ++---- tests/test_utf8.py | 17 ++-- tests/test_vhosts.py | 13 ++- tests/test_views.py | 38 +++----- 28 files changed, 253 insertions(+), 510 deletions(-) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..5844e3a1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +import pytest + +from sanic import Sanic + + +@pytest.fixture +def app(request): + return Sanic(request.node.name) diff --git a/tests/test_bad_request.py b/tests/test_bad_request.py index bf595085..eed4e83a 100644 --- a/tests/test_bad_request.py +++ b/tests/test_bad_request.py @@ -1,9 +1,7 @@ import asyncio -from sanic import Sanic -def test_bad_request_response(): - app = Sanic('test_bad_request_response') +def test_bad_request_response(app): lines = [] @app.listener('after_server_start') async def _request(sanic, loop): diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 37756085..4b821e91 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -3,9 +3,8 @@ import inspect import os import pytest -from sanic import Sanic from sanic.blueprints import Blueprint -from sanic.response import json, text +from sanic.response import text from sanic.exceptions import NotFound, ServerError, InvalidUsage from sanic.constants import HTTP_METHODS @@ -23,8 +22,7 @@ def get_file_content(static_file_directory, file_name): return file.read() @pytest.mark.parametrize('method', HTTP_METHODS) -def test_versioned_routes_get(method): - app = Sanic('test_shorhand_routes_get') +def test_versioned_routes_get(app, method): bp = Blueprint('test_text') method = method.lower() @@ -46,8 +44,7 @@ def test_versioned_routes_get(method): assert response.status == 200 -def test_bp(): - app = Sanic('test_text') +def test_bp(app): bp = Blueprint('test_text') @bp.route('/') @@ -60,8 +57,7 @@ def test_bp(): assert response.text == 'Hello' -def test_bp_strict_slash(): - app = Sanic('test_route_strict_slash') +def test_bp_strict_slash(app): bp = Blueprint('test_text') @bp.get('/get', strict_slashes=True) @@ -87,8 +83,7 @@ def test_bp_strict_slash(): request, response = app.test_client.post('/post') assert response.status == 404 -def test_bp_strict_slash_default_value(): - app = Sanic('test_route_strict_slash') +def test_bp_strict_slash_default_value(app): bp = Blueprint('test_text', strict_slashes=True) @bp.get('/get') @@ -107,8 +102,7 @@ def test_bp_strict_slash_default_value(): request, response = app.test_client.post('/post') assert response.status == 404 -def test_bp_strict_slash_without_passing_default_value(): - app = Sanic('test_route_strict_slash') +def test_bp_strict_slash_without_passing_default_value(app): bp = Blueprint('test_text') @bp.get('/get') @@ -127,8 +121,7 @@ def test_bp_strict_slash_without_passing_default_value(): request, response = app.test_client.post('/post') assert response.text == 'OK' -def test_bp_strict_slash_default_value_can_be_overwritten(): - app = Sanic('test_route_strict_slash') +def test_bp_strict_slash_default_value_can_be_overwritten(app): bp = Blueprint('test_text', strict_slashes=True) @bp.get('/get', strict_slashes=False) @@ -147,8 +140,7 @@ def test_bp_strict_slash_default_value_can_be_overwritten(): request, response = app.test_client.post('/post') assert response.text == 'OK' -def test_bp_with_url_prefix(): - app = Sanic('test_text') +def test_bp_with_url_prefix(app): bp = Blueprint('test_text', url_prefix='/test1') @bp.route('/') @@ -161,8 +153,7 @@ def test_bp_with_url_prefix(): assert response.text == 'Hello' -def test_several_bp_with_url_prefix(): - app = Sanic('test_text') +def test_several_bp_with_url_prefix(app): bp = Blueprint('test_text', url_prefix='/test1') bp2 = Blueprint('test_text2', url_prefix='/test2') @@ -182,8 +173,7 @@ def test_several_bp_with_url_prefix(): request, response = app.test_client.get('/test2/') assert response.text == 'Hello2' -def test_bp_with_host(): - app = Sanic('test_bp_host') +def test_bp_with_host(app): bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com") @bp.route('/') @@ -209,8 +199,7 @@ def test_bp_with_host(): assert response.text == 'Hello subdomain!' -def test_several_bp_with_host(): - app = Sanic('test_text') +def test_several_bp_with_host(app): bp = Blueprint('test_text', url_prefix='/test', host="example.com") @@ -253,8 +242,7 @@ def test_several_bp_with_host(): headers=headers) assert response.text == 'Hello3' -def test_bp_middleware(): - app = Sanic('test_middleware') +def test_bp_middleware(app): blueprint = Blueprint('test_middleware') @blueprint.middleware('response') @@ -272,8 +260,7 @@ def test_bp_middleware(): assert response.status == 200 assert response.text == 'OK' -def test_bp_exception_handler(): - app = Sanic('test_middleware') +def test_bp_exception_handler(app): blueprint = Blueprint('test_middleware') @blueprint.route('/1') @@ -305,8 +292,7 @@ def test_bp_exception_handler(): request, response = app.test_client.get('/3') assert response.status == 200 -def test_bp_listeners(): - app = Sanic('test_middleware') +def test_bp_listeners(app): blueprint = Blueprint('test_middleware') order = [] @@ -341,12 +327,11 @@ def test_bp_listeners(): assert order == [1,2,3,4,5,6] -def test_bp_static(): +def test_bp_static(app): current_file = inspect.getfile(inspect.currentframe()) with open(current_file, 'rb') as file: current_file_contents = file.read() - app = Sanic('test_static') blueprint = Blueprint('test_static') blueprint.static('/testing.file', current_file) @@ -358,13 +343,12 @@ def test_bp_static(): assert response.body == current_file_contents @pytest.mark.parametrize('file_name', ['test.html']) -def test_bp_static_content_type(file_name): +def test_bp_static_content_type(app, file_name): # This is done here, since no other test loads a file here current_file = inspect.getfile(inspect.currentframe()) current_directory = os.path.dirname(os.path.abspath(current_file)) static_directory = os.path.join(current_directory, 'static') - app = Sanic('test_static') blueprint = Blueprint('test_static') blueprint.static( '/testing.file', @@ -379,8 +363,7 @@ def test_bp_static_content_type(file_name): assert response.body == get_file_content(static_directory, file_name) assert response.headers['Content-Type'] == 'text/html; charset=utf-8' -def test_bp_shorthand(): - app = Sanic('test_shorhand_routes') +def test_bp_shorthand(app): blueprint = Blueprint('test_shorhand_routes') ev = asyncio.Event() @@ -478,9 +461,7 @@ def test_bp_shorthand(): assert response.status == 101 assert ev.is_set() -def test_bp_group(): - app = Sanic('test_nested_bp_groups') - +def test_bp_group(app): deep_0 = Blueprint('deep_0', url_prefix='/deep') deep_1 = Blueprint('deep_1', url_prefix = '/deep1') diff --git a/tests/test_config.py b/tests/test_config.py index e393d02b..3db35f4d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,8 +5,7 @@ from tempfile import NamedTemporaryFile from sanic import Sanic -def test_load_from_object(): - app = Sanic('test_load_from_object') +def test_load_from_object(app): class Config: not_for_config = 'should not be used' CONFIG_VALUE = 'should be used' @@ -34,8 +33,7 @@ def test_load_env_prefix(): assert app.config.TEST_ANSWER == 42 del environ["MYAPP_TEST_ANSWER"] -def test_load_from_file(): - app = Sanic('test_load_from_file') +def test_load_from_file(app): config = b""" VALUE = 'some value' condition = 1 == 1 @@ -53,14 +51,12 @@ if condition: assert 'condition' not in app.config -def test_load_from_missing_file(): - app = Sanic('test_load_from_missing_file') +def test_load_from_missing_file(app): with pytest.raises(IOError): app.config.from_pyfile('non-existent file') -def test_load_from_envvar(): - app = Sanic('test_load_from_envvar') +def test_load_from_envvar(app): config = b"VALUE = 'some value'" with NamedTemporaryFile() as config_file: config_file.write(config) @@ -71,14 +67,12 @@ def test_load_from_envvar(): assert app.config.VALUE == 'some value' -def test_load_from_missing_envvar(): - app = Sanic('test_load_from_missing_envvar') +def test_load_from_missing_envvar(app): with pytest.raises(RuntimeError): app.config.from_envvar('non-existent variable') -def test_overwrite_exisiting_config(): - app = Sanic('test_overwrite_exisiting_config') +def test_overwrite_exisiting_config(app): app.config.DEFAULT = 1 class Config: DEFAULT = 2 @@ -87,7 +81,6 @@ def test_overwrite_exisiting_config(): assert app.config.DEFAULT == 2 -def test_missing_config(): - app = Sanic('test_missing_config') +def test_missing_config(app): with pytest.raises(AttributeError): app.config.NON_EXISTENT diff --git a/tests/test_cookies.py b/tests/test_cookies.py index 61f50735..87d7dcad 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -9,8 +9,7 @@ import pytest # GET # ------------------------------------------------------------ # -def test_cookies(): - app = Sanic('test_text') +def test_cookies(app): @app.route('/') def handler(request): @@ -30,8 +29,7 @@ def test_cookies(): (False, False), (True, True), ]) -def test_false_cookies_encoded(httponly, expected): - app = Sanic('test_text') +def test_false_cookies_encoded(app, httponly, expected): @app.route('/') def handler(request): @@ -49,8 +47,7 @@ def test_false_cookies_encoded(httponly, expected): (False, False), (True, True), ]) -def test_false_cookies(httponly, expected): - app = Sanic('test_text') +def test_false_cookies(app, httponly, expected): @app.route('/') def handler(request): @@ -65,8 +62,7 @@ def test_false_cookies(httponly, expected): assert ('HttpOnly' in response_cookies['right_back'].output()) == expected -def test_http2_cookies(): - app = Sanic('test_http2_cookies') +def test_http2_cookies(app): @app.route('/') async def handler(request): @@ -78,8 +74,7 @@ def test_http2_cookies(): assert response.text == 'Cookies are: working!' -def test_cookie_options(): - app = Sanic('test_text') +def test_cookie_options(app): @app.route('/') def handler(request): @@ -96,8 +91,7 @@ def test_cookie_options(): assert response_cookies['test'].value == 'at you' assert response_cookies['test']['httponly'] == True -def test_cookie_deletion(): - app = Sanic('test_text') +def test_cookie_deletion(app): @app.route('/') def handler(request): diff --git a/tests/test_create_task.py b/tests/test_create_task.py index 1517ca8c..3a94884d 100644 --- a/tests/test_create_task.py +++ b/tests/test_create_task.py @@ -1,18 +1,16 @@ -from sanic import Sanic from sanic.response import text from threading import Event import asyncio from queue import Queue -def test_create_task(): +def test_create_task(app): e = Event() async def coro(): await asyncio.sleep(0.05) e.set() - app = Sanic('test_create_task') app.add_task(coro) @app.route('/early') @@ -30,8 +28,7 @@ def test_create_task(): request, response = app.test_client.get('/late') assert response.body == b'True' -def test_create_task_with_app_arg(): - app = Sanic('test_add_task') +def test_create_task_with_app_arg(app): q = Queue() @app.route('/') @@ -44,4 +41,4 @@ def test_create_task_with_app_arg(): app.add_task(coro) request, response = app.test_client.get('/') - assert q.get() == 'test_add_task' + assert q.get() == 'test_create_task_with_app_arg' diff --git a/tests/test_custom_protocol.py b/tests/test_custom_protocol.py index 74564012..236eb831 100644 --- a/tests/test_custom_protocol.py +++ b/tests/test_custom_protocol.py @@ -1,9 +1,6 @@ -from sanic import Sanic from sanic.server import HttpProtocol from sanic.response import text -app = Sanic('test_custom_porotocol') - class CustomHttpProtocol(HttpProtocol): @@ -16,12 +13,12 @@ class CustomHttpProtocol(HttpProtocol): self.transport.close() -@app.route('/1') -async def handler_1(request): - return 'OK' +def test_use_custom_protocol(app): + @app.route('/1') + async def handler_1(request): + return 'OK' -def test_use_custom_protocol(): server_kwargs = { 'protocol': CustomHttpProtocol } diff --git a/tests/test_dynamic_routes.py b/tests/test_dynamic_routes.py index 950584a8..a37861e3 100644 --- a/tests/test_dynamic_routes.py +++ b/tests/test_dynamic_routes.py @@ -10,8 +10,7 @@ import pytest ("put", "text", "OK2 test"), ("delete", "status", 405), ]) -def test_overload_dynamic_routes(method, attr, expected): - app = Sanic('test_dynamic_route') +def test_overload_dynamic_routes(app, method, attr, expected): @app.route('/overload/', methods=['GET']) async def handler1(request, param): @@ -25,8 +24,7 @@ def test_overload_dynamic_routes(method, attr, expected): assert getattr(response, attr) == expected -def test_overload_dynamic_routes_exist(): - app = Sanic('test_dynamic_route') +def test_overload_dynamic_routes_exist(app): @app.route('/overload/', methods=['GET']) async def handler1(request, param): diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index b4e2c6ea..cf41982d 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -81,8 +81,7 @@ def exception_app(): return app -def test_catch_exception_list(): - app = Sanic('exception_list') +def test_catch_exception_list(app): @app.exception([SanicExceptionTestException, NotFound]) def exception_list(request, exception): diff --git a/tests/test_logging.py b/tests/test_logging.py index 1a040a5c..3af3f122 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -23,7 +23,7 @@ def reset_logging(): reload(logging) -def test_log(): +def test_log(app): log_stream = StringIO() for handler in logging.root.handlers[:]: logging.root.removeHandler(handler) @@ -33,7 +33,6 @@ def test_log(): stream=log_stream ) log = logging.getLogger() - app = Sanic('test_logging') rand_string = str(uuid.uuid4()) @app.route('/') @@ -80,9 +79,8 @@ def test_logging_pass_customer_logconfig(): @pytest.mark.parametrize('debug', (True, False, )) -def test_log_connection_lost(debug, monkeypatch): +def test_log_connection_lost(app, debug, monkeypatch): """ Should not log Connection lost exception on non debug """ - app = Sanic('connection_lost') stream = StringIO() root = logging.getLogger('root') root.addHandler(logging.StreamHandler(stream)) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 4d4d6901..d2098cd1 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1,7 +1,5 @@ -from json import loads as json_loads, dumps as json_dumps -from sanic import Sanic from sanic.request import Request -from sanic.response import json, text, HTTPResponse +from sanic.response import text, HTTPResponse from sanic.exceptions import NotFound @@ -9,9 +7,7 @@ from sanic.exceptions import NotFound # GET # ------------------------------------------------------------ # -def test_middleware_request(): - app = Sanic('test_middleware_request') - +def test_middleware_request(app): results = [] @app.middleware @@ -28,9 +24,7 @@ def test_middleware_request(): assert type(results[0]) is Request -def test_middleware_response(): - app = Sanic('test_middleware_response') - +def test_middleware_response(app): results = [] @app.middleware('request') @@ -54,8 +48,7 @@ def test_middleware_response(): assert isinstance(results[2], HTTPResponse) -def test_middleware_response_exception(): - app = Sanic('test_middleware_response_exception') +def test_middleware_response_exception(app): result = {'status_code': None} @app.middleware('response') @@ -75,8 +68,7 @@ def test_middleware_response_exception(): assert response.text == 'OK' assert result['status_code'] == 404 -def test_middleware_override_request(): - app = Sanic('test_middleware_override_request') +def test_middleware_override_request(app): @app.middleware async def halt_request(request): @@ -92,8 +84,7 @@ def test_middleware_override_request(): assert response.text == 'OK' -def test_middleware_override_response(): - app = Sanic('test_middleware_override_response') +def test_middleware_override_response(app): @app.middleware('response') async def process_response(request, response): @@ -109,10 +100,7 @@ def test_middleware_override_response(): assert response.text == 'OK' - -def test_middleware_order(): - app = Sanic('test_middleware_order') - +def test_middleware_order(app): order = [] @app.middleware('request') diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index 7d94a972..c78e19fd 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -2,15 +2,13 @@ import multiprocessing import random import signal -from sanic import Sanic from sanic.testing import HOST, PORT -def test_multiprocessing(): +def test_multiprocessing(app): """Tests that the number of children we produce is correct""" # Selects a number at random so we can spot check num_workers = random.choice(range(2, multiprocessing.cpu_count() * 2 + 1)) - app = Sanic('test_multiprocessing') process_list = set() def stop_on_alarm(*args): diff --git a/tests/test_named_routes.py b/tests/test_named_routes.py index ca377e8d..65d5955a 100644 --- a/tests/test_named_routes.py +++ b/tests/test_named_routes.py @@ -4,7 +4,6 @@ import asyncio import pytest -from sanic import Sanic from sanic.blueprints import Blueprint from sanic.response import text from sanic.exceptions import URLBuildError @@ -16,8 +15,7 @@ from sanic.constants import HTTP_METHODS # ------------------------------------------------------------ # @pytest.mark.parametrize('method', HTTP_METHODS) -def test_versioned_named_routes_get(method): - app = Sanic('test_shorhand_routes_get') +def test_versioned_named_routes_get(app, method): bp = Blueprint('test_bp', url_prefix='/bp') method = method.lower() @@ -57,8 +55,7 @@ def test_versioned_named_routes_get(method): app.url_for('handler') -def test_shorthand_default_routes_get(): - app = Sanic('test_shorhand_routes_get') +def test_shorthand_default_routes_get(app): @app.get('/get') def handler(request): @@ -68,8 +65,7 @@ def test_shorthand_default_routes_get(): assert app.url_for('handler') == '/get' -def test_shorthand_named_routes_get(): - app = Sanic('test_shorhand_routes_get') +def test_shorthand_named_routes_get(app): bp = Blueprint('test_bp', url_prefix='/bp') @app.get('/get', name='route_get') @@ -93,8 +89,7 @@ def test_shorthand_named_routes_get(): app.url_for('test_bp.handler2') -def test_shorthand_named_routes_post(): - app = Sanic('test_shorhand_routes_post') +def test_shorthand_named_routes_post(app): @app.post('/post', name='route_name') def handler(request): @@ -106,8 +101,7 @@ def test_shorthand_named_routes_post(): app.url_for('handler') -def test_shorthand_named_routes_put(): - app = Sanic('test_shorhand_routes_put') +def test_shorthand_named_routes_put(app): @app.put('/put', name='route_put') def handler(request): @@ -121,8 +115,7 @@ def test_shorthand_named_routes_put(): app.url_for('handler') -def test_shorthand_named_routes_delete(): - app = Sanic('test_shorhand_routes_delete') +def test_shorthand_named_routes_delete(app): @app.delete('/delete', name='route_delete') def handler(request): @@ -136,8 +129,7 @@ def test_shorthand_named_routes_delete(): app.url_for('handler') -def test_shorthand_named_routes_patch(): - app = Sanic('test_shorhand_routes_patch') +def test_shorthand_named_routes_patch(app): @app.patch('/patch', name='route_patch') def handler(request): @@ -151,8 +143,7 @@ def test_shorthand_named_routes_patch(): app.url_for('handler') -def test_shorthand_named_routes_head(): - app = Sanic('test_shorhand_routes_head') +def test_shorthand_named_routes_head(app): @app.head('/head', name='route_head') def handler(request): @@ -166,8 +157,7 @@ def test_shorthand_named_routes_head(): app.url_for('handler') -def test_shorthand_named_routes_options(): - app = Sanic('test_shorhand_routes_options') +def test_shorthand_named_routes_options(app): @app.options('/options', name='route_options') def handler(request): @@ -181,8 +171,7 @@ def test_shorthand_named_routes_options(): app.url_for('handler') -def test_named_static_routes(): - app = Sanic('test_dynamic_route') +def test_named_static_routes(app): @app.route('/test', name='route_test') async def handler1(request): @@ -205,9 +194,7 @@ def test_named_static_routes(): app.url_for('handler2') -def test_named_dynamic_route(): - app = Sanic('test_dynamic_route') - +def test_named_dynamic_route(app): results = [] @app.route('/folder/', name='route_dynamic') @@ -221,8 +208,7 @@ def test_named_dynamic_route(): app.url_for('handler') -def test_dynamic_named_route_regex(): - app = Sanic('test_dynamic_route_regex') +def test_dynamic_named_route_regex(app): @app.route('/folder/', name='route_re') async def handler(request, folder_id): @@ -235,8 +221,7 @@ def test_dynamic_named_route_regex(): app.url_for('handler') -def test_dynamic_named_route_path(): - app = Sanic('test_dynamic_route_path') +def test_dynamic_named_route_path(app): @app.route('//info', name='route_dynamic_path') async def handler(request, path): @@ -249,8 +234,7 @@ def test_dynamic_named_route_path(): app.url_for('handler') -def test_dynamic_named_route_unhashable(): - app = Sanic('test_dynamic_route_unhashable') +def test_dynamic_named_route_unhashable(app): @app.route('/folder//end/', name='route_unhashable') @@ -265,8 +249,7 @@ def test_dynamic_named_route_unhashable(): app.url_for('handler') -def test_websocket_named_route(): - app = Sanic('test_websocket_route') +def test_websocket_named_route(app): ev = asyncio.Event() @app.websocket('/ws', name='route_ws') @@ -280,8 +263,7 @@ def test_websocket_named_route(): app.url_for('handler') -def test_websocket_named_route_with_subprotocols(): - app = Sanic('test_websocket_route') +def test_websocket_named_route_with_subprotocols(app): results = [] @app.websocket('/ws', subprotocols=['foo', 'bar'], name='route_ws') @@ -294,8 +276,7 @@ def test_websocket_named_route_with_subprotocols(): app.url_for('handler') -def test_static_add_named_route(): - app = Sanic('test_static_add_route') +def test_static_add_named_route(app): async def handler1(request): return text('OK1') @@ -319,9 +300,7 @@ def test_static_add_named_route(): app.url_for('handler2') -def test_dynamic_add_named_route(): - app = Sanic('test_dynamic_add_route') - +def test_dynamic_add_named_route(app): results = [] async def handler(request, name): @@ -335,8 +314,7 @@ def test_dynamic_add_named_route(): app.url_for('handler') -def test_dynamic_add_named_route_unhashable(): - app = Sanic('test_dynamic_add_route_unhashable') +def test_dynamic_add_named_route_unhashable(app): async def handler(request, unhashable): return text('OK') @@ -351,8 +329,7 @@ def test_dynamic_add_named_route_unhashable(): app.url_for('handler') -def test_overload_routes(): - app = Sanic('test_dynamic_route') +def test_overload_routes(app): @app.route('/overload', methods=['GET'], name='route_first') async def handler1(request): diff --git a/tests/test_payload_too_large.py b/tests/test_payload_too_large.py index ecac605c..49ad5ab7 100644 --- a/tests/test_payload_too_large.py +++ b/tests/test_payload_too_large.py @@ -1,49 +1,45 @@ -from sanic import Sanic from sanic.exceptions import PayloadTooLarge from sanic.response import text -def test_payload_too_large_from_error_handler(): - data_received_app = Sanic('data_received') - data_received_app.config.REQUEST_MAX_SIZE = 1 +def test_payload_too_large_from_error_handler(app): + app.config.REQUEST_MAX_SIZE = 1 - @data_received_app.route('/1') + @app.route('/1') async def handler1(request): return text('OK') - @data_received_app.exception(PayloadTooLarge) + @app.exception(PayloadTooLarge) def handler_exception(request, exception): return text('Payload Too Large from error_handler.', 413) - response = data_received_app.test_client.get('/1', gather_request=False) + response = app.test_client.get('/1', gather_request=False) assert response.status == 413 assert response.text == 'Payload Too Large from error_handler.' -def test_payload_too_large_at_data_received_default(): - data_received_default_app = Sanic('data_received_default') - data_received_default_app.config.REQUEST_MAX_SIZE = 1 +def test_payload_too_large_at_data_received_default(app): + app.config.REQUEST_MAX_SIZE = 1 - @data_received_default_app.route('/1') + @app.route('/1') async def handler2(request): return text('OK') - response = data_received_default_app.test_client.get( + response = app.test_client.get( '/1', gather_request=False) assert response.status == 413 assert response.text == 'Error: Payload Too Large' -def test_payload_too_large_at_on_header_default(): - on_header_default_app = Sanic('on_header') - on_header_default_app.config.REQUEST_MAX_SIZE = 500 +def test_payload_too_large_at_on_header_default(app): + app.config.REQUEST_MAX_SIZE = 500 - @on_header_default_app.post('/1') + @app.post('/1') async def handler3(request): return text('OK') data = 'a' * 1000 - response = on_header_default_app.test_client.post( + response = app.test_client.post( '/1', gather_request=False, data=data) assert response.status == 413 assert response.text == 'Error: Payload Too Large' diff --git a/tests/test_redirect.py b/tests/test_redirect.py index 5fdec2a6..f5efac60 100644 --- a/tests/test_redirect.py +++ b/tests/test_redirect.py @@ -1,12 +1,10 @@ import pytest -from sanic import Sanic from sanic.response import text, redirect @pytest.fixture -def redirect_app(): - app = Sanic('test_redirection') +def redirect_app(app): @app.route('/redirect_init') async def redirect_init(request): diff --git a/tests/test_request_data.py b/tests/test_request_data.py index f795ff1f..69935fc0 100644 --- a/tests/test_request_data.py +++ b/tests/test_request_data.py @@ -1,6 +1,5 @@ import random -from sanic import Sanic from sanic.response import json try: @@ -9,8 +8,7 @@ except ImportError: from json import loads -def test_storage(): - app = Sanic('test_text') +def test_storage(app): @app.middleware('request') def store(request): @@ -29,8 +27,7 @@ def test_storage(): assert response_json.get('sidekick') is None -def test_app_injection(): - app = Sanic('test_app_injection') +def test_app_injection(app): expected = random.choice(range(0, 100)) @app.listener('after_server_start') diff --git a/tests/test_request_stream.py b/tests/test_request_stream.py index b14aa519..97cd5a4a 100644 --- a/tests/test_request_stream.py +++ b/tests/test_request_stream.py @@ -1,5 +1,4 @@ import asyncio -from sanic import Sanic from sanic.blueprints import Blueprint from sanic.views import CompositionView from sanic.views import HTTPMethodView @@ -9,11 +8,9 @@ from sanic.response import stream, text data = "abc" * 100000 -def test_request_stream_method_view(): +def test_request_stream_method_view(app): '''for self.is_request_stream = True''' - app = Sanic('test_request_stream_method_view') - class SimpleView(HTTPMethodView): def get(self, request): @@ -44,11 +41,9 @@ def test_request_stream_method_view(): assert response.text == data -def test_request_stream_app(): +def test_request_stream_app(app): '''for self.is_request_stream = True and decorators''' - app = Sanic('test_request_stream_app') - @app.get('/get') async def get(request): assert request.stream is None @@ -163,11 +158,9 @@ def test_request_stream_app(): assert response.text == data -def test_request_stream_handle_exception(): +def test_request_stream_handle_exception(app): '''for handling exceptions properly''' - app = Sanic('test_request_stream_exception') - @app.post('/post/', stream=True) async def post(request, id): assert isinstance(request.stream, asyncio.Queue) @@ -191,10 +184,8 @@ def test_request_stream_handle_exception(): assert response.text == 'Error: Method GET not allowed for URL /post/random_id' -def test_request_stream_blueprint(): +def test_request_stream_blueprint(app): '''for self.is_request_stream = True''' - - app = Sanic('test_request_stream_blueprint') bp = Blueprint('test_blueprint_request_stream_blueprint') @app.get('/get') @@ -313,11 +304,9 @@ def test_request_stream_blueprint(): assert response.text == data -def test_request_stream_composition_view(): +def test_request_stream_composition_view(app): '''for self.is_request_stream = True''' - app = Sanic('test_request_stream__composition_view') - def get_handler(request): assert request.stream is None return text('OK') @@ -348,11 +337,9 @@ def test_request_stream_composition_view(): assert response.text == data -def test_request_stream(): +def test_request_stream(app): '''test for complex application''' - bp = Blueprint('test_blueprint_request_stream') - app = Sanic('test_request_stream') class SimpleView(HTTPMethodView): diff --git a/tests/test_requests.py b/tests/test_requests.py index 2a91fb9b..9617216e 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -5,7 +5,6 @@ import ssl import pytest -from sanic import Sanic from sanic.exceptions import ServerError from sanic.response import json, text from sanic.request import DEFAULT_HTTP_CONTENT_TYPE @@ -16,8 +15,7 @@ from sanic.testing import HOST, PORT # GET # ------------------------------------------------------------ # -def test_sync(): - app = Sanic('test_text') +def test_sync(app): @app.route('/') def handler(request): @@ -27,8 +25,8 @@ def test_sync(): assert response.text == 'Hello' -def test_remote_address(): - app = Sanic('test_text') + +def test_remote_address(app): @app.route('/') def handler(request): @@ -38,8 +36,8 @@ def test_remote_address(): assert response.text == '127.0.0.1' -def test_text(): - app = Sanic('test_text') + +def test_text(app): @app.route('/') async def handler(request): @@ -50,8 +48,7 @@ def test_text(): assert response.text == 'Hello' -def test_headers(): - app = Sanic('test_text') +def test_headers(app): @app.route('/') async def handler(request): @@ -63,8 +60,7 @@ def test_headers(): assert response.headers.get('spam') == 'great' -def test_non_str_headers(): - app = Sanic('test_text') +def test_non_str_headers(app): @app.route('/') async def handler(request): @@ -75,8 +71,8 @@ def test_non_str_headers(): assert response.headers.get('answer') == '42' -def test_invalid_response(): - app = Sanic('test_invalid_response') + +def test_invalid_response(app): @app.exception(ServerError) def handler_exception(request, exception): @@ -91,8 +87,7 @@ def test_invalid_response(): assert response.text == "Internal Server Error." -def test_json(): - app = Sanic('test_json') +def test_json(app): @app.route('/') async def handler(request): @@ -105,8 +100,7 @@ def test_json(): assert results.get('test') == True -def test_empty_json(): - app = Sanic('test_json') +def test_empty_json(app): @app.route('/') async def handler(request): @@ -118,8 +112,7 @@ def test_empty_json(): assert response.text == 'null' -def test_invalid_json(): - app = Sanic('test_json') +def test_invalid_json(app): @app.route('/') async def handler(request): @@ -131,8 +124,7 @@ def test_invalid_json(): assert response.status == 400 -def test_query_string(): - app = Sanic('test_query_string') +def test_query_string(app): @app.route('/') async def handler(request): @@ -145,8 +137,7 @@ def test_query_string(): assert request.args.get('test2') == 'false' -def test_uri_template(): - app = Sanic('test_uri_template') +def test_uri_template(app): @app.route('/foo//bar/') async def handler(request): @@ -156,8 +147,7 @@ def test_uri_template(): assert request.uri_template == '/foo//bar/' -def test_token(): - app = Sanic('test_post_token') +def test_token(app): @app.route('/') async def handler(request): @@ -204,8 +194,7 @@ def test_token(): assert request.token is None -def test_content_type(): - app = Sanic('test_content_type') +def test_content_type(app): @app.route('/') async def handler(request): @@ -223,8 +212,7 @@ def test_content_type(): assert response.text == 'application/json' -def test_remote_addr(): - app = Sanic('test_content_type') +def test_remote_addr(app): @app.route('/') async def handler(request): @@ -249,8 +237,7 @@ def test_remote_addr(): assert response.text == '127.0.0.1' -def test_match_info(): - app = Sanic('test_match_info') +def test_match_info(app): @app.route('/api/v1/user//') async def handler(request, user_id): @@ -266,8 +253,7 @@ def test_match_info(): # POST # ------------------------------------------------------------ # -def test_post_json(): - app = Sanic('test_post_json') +def test_post_json(app): @app.route('/', methods=['POST']) async def handler(request): @@ -283,8 +269,7 @@ def test_post_json(): assert response.text == 'OK' -def test_post_form_urlencoded(): - app = Sanic('test_post_form_urlencoded') +def test_post_form_urlencoded(app): @app.route('/', methods=['POST']) async def handler(request): @@ -311,8 +296,7 @@ def test_post_form_urlencoded(): 'OK\r\n' \ '------sanic--\r\n', ]) -def test_post_form_multipart_form_data(payload): - app = Sanic('test_post_form_multipart_form_data') +def test_post_form_multipart_form_data(app, payload): @app.route('/', methods=['POST']) async def handler(request): @@ -331,8 +315,7 @@ def test_post_form_multipart_form_data(payload): ('/bar/baz', '', 'http://{}:{}/bar/baz'), ('/moo/boo', 'arg1=val1', 'http://{}:{}/moo/boo?arg1=val1') ]) -def test_url_attributes_no_ssl(path, query, expected_url): - app = Sanic('test_url_attrs_no_ssl') +def test_url_attributes_no_ssl(app, path, query, expected_url): async def handler(request): return text('OK') @@ -356,9 +339,7 @@ def test_url_attributes_no_ssl(path, query, expected_url): ('/bar/baz', '', 'https://{}:{}/bar/baz'), ('/moo/boo', 'arg1=val1', 'https://{}:{}/moo/boo?arg1=val1') ]) -def test_url_attributes_with_ssl(path, query, expected_url): - app = Sanic('test_url_attrs_with_ssl') - +def test_url_attributes_with_ssl(app, path, query, expected_url): current_dir = os.path.dirname(os.path.realpath(__file__)) context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) context.load_cert_chain( diff --git a/tests/test_response.py b/tests/test_response.py index 6dcd2ea6..78f9f103 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -8,7 +8,6 @@ from urllib.parse import unquote import pytest from random import choice -from sanic import Sanic from sanic.response import HTTPResponse, stream, StreamingHTTPResponse, file, file_stream, json from sanic.server import HttpProtocol from sanic.testing import HOST, PORT @@ -17,9 +16,8 @@ from unittest.mock import MagicMock JSON_DATA = {'ok': True} -def test_response_body_not_a_string(): +def test_response_body_not_a_string(app): """Test when a response body sent from the application is not a string""" - app = Sanic('response_body_not_a_string') random_num = choice(range(1000)) @app.route('/hello') @@ -36,9 +34,7 @@ async def sample_streaming_fn(response): await response.write('bar') - -def test_method_not_allowed(): - app = Sanic('method_not_allowed') +def test_method_not_allowed(app): @app.get('/') async def test(request): @@ -66,8 +62,8 @@ def test_method_not_allowed(): assert response.headers['Content-Length'] == '0' -def test_response_header(): - app = Sanic('test_response_header') +def test_response_header(app): + @app.get('/') async def test(request): return json({ @@ -86,8 +82,7 @@ def test_response_header(): @pytest.fixture -def json_app(): - app = Sanic('json') +def json_app(app): @app.route("/") async def test(request): @@ -145,8 +140,7 @@ def test_no_content(json_app): @pytest.fixture -def streaming_app(): - app = Sanic('streaming') +def streaming_app(app): @app.route("/") async def test(request): @@ -240,8 +234,7 @@ def get_file_content(static_file_directory, file_name): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) @pytest.mark.parametrize('status', [200, 401]) -def test_file_response(file_name, static_file_directory, status): - app = Sanic('test_file_helper') +def test_file_response(app, file_name, static_file_directory, status): @app.route('/files/', methods=['GET']) def file_route(request, filename): @@ -258,8 +251,7 @@ def test_file_response(file_name, static_file_directory, status): @pytest.mark.parametrize('source,dest', [ ('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')]) -def test_file_response_custom_filename(source, dest, static_file_directory): - app = Sanic('test_file_helper') +def test_file_response_custom_filename(app, source, dest, static_file_directory): @app.route('/files/', methods=['GET']) def file_route(request, filename): @@ -274,8 +266,7 @@ def test_file_response_custom_filename(source, dest, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_file_head_response(file_name, static_file_directory): - app = Sanic('test_file_helper') +def test_file_head_response(app, file_name, static_file_directory): @app.route('/files/', methods=['GET', 'HEAD']) async def file_route(request, filename): @@ -303,8 +294,7 @@ def test_file_head_response(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) -def test_file_stream_response(file_name, static_file_directory): - app = Sanic('test_file_helper') +def test_file_stream_response(app, file_name, static_file_directory): @app.route('/files/', methods=['GET']) def file_route(request, filename): @@ -321,8 +311,7 @@ def test_file_stream_response(file_name, static_file_directory): @pytest.mark.parametrize('source,dest', [ ('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')]) -def test_file_stream_response_custom_filename(source, dest, static_file_directory): - app = Sanic('test_file_helper') +def test_file_stream_response_custom_filename(app, source, dest, static_file_directory): @app.route('/files/', methods=['GET']) def file_route(request, filename): @@ -337,8 +326,7 @@ def test_file_stream_response_custom_filename(source, dest, static_file_director @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_file_stream_head_response(file_name, static_file_directory): - app = Sanic('test_file_helper') +def test_file_stream_head_response(app, file_name, static_file_directory): @app.route('/files/', methods=['GET', 'HEAD']) async def file_route(request, filename): diff --git a/tests/test_routes.py b/tests/test_routes.py index 146db97c..d70bf975 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -12,9 +12,7 @@ from sanic.constants import HTTP_METHODS # ------------------------------------------------------------ # @pytest.mark.parametrize('method', HTTP_METHODS) -def test_versioned_routes_get(method): - app = Sanic('test_shorhand_routes_get') - +def test_versioned_routes_get(app, method): method = method.lower() func = getattr(app, method) @@ -32,8 +30,7 @@ def test_versioned_routes_get(method): assert response.status == 200 -def test_shorthand_routes_get(): - app = Sanic('test_shorhand_routes_get') +def test_shorthand_routes_get(app): @app.get('/get') def handler(request): @@ -46,8 +43,7 @@ def test_shorthand_routes_get(): assert response.status == 405 -def test_shorthand_routes_multiple(): - app = Sanic('test_shorthand_routes_multiple') +def test_shorthand_routes_multiple(app): @app.get('/get') def get_handler(request): @@ -65,8 +61,7 @@ def test_shorthand_routes_multiple(): assert response.status == 200 -def test_route_strict_slash(): - app = Sanic('test_route_strict_slash') +def test_route_strict_slash(app): @app.get('/get', strict_slashes=True) def handler(request): @@ -93,9 +88,8 @@ def test_route_strict_slash(): assert response.status == 404 -def test_route_invalid_parameter_syntax(): +def test_route_invalid_parameter_syntax(app): with pytest.raises(ValueError): - app = Sanic('test_route_invalid_param_syntax') @app.get('/get/<:string>', strict_slashes=True) def handler(request): @@ -115,8 +109,7 @@ def test_route_strict_slash_default_value(): assert response.status == 404 -def test_route_strict_slash_without_passing_default_value(): - app = Sanic('test_route_strict_slash') +def test_route_strict_slash_without_passing_default_value(app): @app.get('/get') def handler(request): @@ -137,8 +130,7 @@ def test_route_strict_slash_default_value_can_be_overwritten(): assert response.text == 'OK' -def test_route_slashes_overload(): - app = Sanic('test_route_slashes_overload') +def test_route_slashes_overload(app): @app.get('/hello/') def handler(request): @@ -161,8 +153,7 @@ def test_route_slashes_overload(): assert response.text == 'OK' -def test_route_optional_slash(): - app = Sanic('test_route_optional_slash') +def test_route_optional_slash(app): @app.get('/get') def handler(request): @@ -174,9 +165,8 @@ def test_route_optional_slash(): request, response = app.test_client.get('/get/') assert response.text == 'OK' -def test_route_strict_slashes_set_to_false_and_host_is_a_list(): +def test_route_strict_slashes_set_to_false_and_host_is_a_list(app): #Part of regression test for issue #1120 - app = Sanic('test_route_strict_slashes_set_to_false_and_host_is_a_list') site1 = 'localhost:{}'.format(app.test_client.port) @@ -209,8 +199,7 @@ def test_route_strict_slashes_set_to_false_and_host_is_a_list(): request, response = app.test_client.delete('http://' + site1 +'/delete') assert response.text == 'OK' -def test_shorthand_routes_post(): - app = Sanic('test_shorhand_routes_post') +def test_shorthand_routes_post(app): @app.post('/post') def handler(request): @@ -223,8 +212,7 @@ def test_shorthand_routes_post(): assert response.status == 405 -def test_shorthand_routes_put(): - app = Sanic('test_shorhand_routes_put') +def test_shorthand_routes_put(app): @app.put('/put') def handler(request): @@ -240,8 +228,7 @@ def test_shorthand_routes_put(): assert response.status == 405 -def test_shorthand_routes_delete(): - app = Sanic('test_shorhand_routes_delete') +def test_shorthand_routes_delete(app): @app.delete('/delete') def handler(request): @@ -257,8 +244,7 @@ def test_shorthand_routes_delete(): assert response.status == 405 -def test_shorthand_routes_patch(): - app = Sanic('test_shorhand_routes_patch') +def test_shorthand_routes_patch(app): @app.patch('/patch') def handler(request): @@ -274,8 +260,7 @@ def test_shorthand_routes_patch(): assert response.status == 405 -def test_shorthand_routes_head(): - app = Sanic('test_shorhand_routes_head') +def test_shorthand_routes_head(app): @app.head('/head') def handler(request): @@ -291,8 +276,7 @@ def test_shorthand_routes_head(): assert response.status == 405 -def test_shorthand_routes_options(): - app = Sanic('test_shorhand_routes_options') +def test_shorthand_routes_options(app): @app.options('/options') def handler(request): @@ -308,8 +292,7 @@ def test_shorthand_routes_options(): assert response.status == 405 -def test_static_routes(): - app = Sanic('test_dynamic_route') +def test_static_routes(app): @app.route('/test') async def handler1(request): @@ -326,9 +309,7 @@ def test_static_routes(): assert response.text == 'OK2' -def test_dynamic_route(): - app = Sanic('test_dynamic_route') - +def test_dynamic_route(app): results = [] @app.route('/folder/') @@ -342,9 +323,7 @@ def test_dynamic_route(): assert results[0] == 'test123' -def test_dynamic_route_string(): - app = Sanic('test_dynamic_route_string') - +def test_dynamic_route_string(app): results = [] @app.route('/folder/') @@ -363,9 +342,7 @@ def test_dynamic_route_string(): assert results[1] == 'favicon.ico' -def test_dynamic_route_int(): - app = Sanic('test_dynamic_route_int') - +def test_dynamic_route_int(app): results = [] @app.route('/folder/') @@ -381,9 +358,7 @@ def test_dynamic_route_int(): assert response.status == 404 -def test_dynamic_route_number(): - app = Sanic('test_dynamic_route_number') - +def test_dynamic_route_number(app): results = [] @app.route('/weight/') @@ -402,8 +377,7 @@ def test_dynamic_route_number(): assert response.status == 404 -def test_dynamic_route_regex(): - app = Sanic('test_dynamic_route_regex') +def test_dynamic_route_regex(app): @app.route('/folder/') async def handler(request, folder_id): @@ -422,9 +396,8 @@ def test_dynamic_route_regex(): assert response.status == 200 -def test_dynamic_route_uuid(): +def test_dynamic_route_uuid(app): import uuid - app = Sanic('test_dynamic_route_uuid') results = [] @@ -444,8 +417,7 @@ def test_dynamic_route_uuid(): assert response.status == 404 -def test_dynamic_route_path(): - app = Sanic('test_dynamic_route_path') +def test_dynamic_route_path(app): @app.route('//info') async def handler(request, path): @@ -468,8 +440,7 @@ def test_dynamic_route_path(): assert response.status == 200 -def test_dynamic_route_unhashable(): - app = Sanic('test_dynamic_route_unhashable') +def test_dynamic_route_unhashable(app): @app.route('/folder//end/') async def handler(request, unhashable): @@ -488,8 +459,7 @@ def test_dynamic_route_unhashable(): assert response.status == 404 -def test_websocket_route(): - app = Sanic('test_websocket_route') +def test_websocket_route(app): ev = asyncio.Event() @app.websocket('/ws') @@ -506,8 +476,7 @@ def test_websocket_route(): assert ev.is_set() -def test_websocket_route_with_subprotocols(): - app = Sanic('test_websocket_route') +def test_websocket_route_with_subprotocols(app): results = [] @app.websocket('/ws', subprotocols=['foo', 'bar']) @@ -548,8 +517,7 @@ def test_websocket_route_with_subprotocols(): assert results == ['bar', 'bar', None, None] -def test_route_duplicate(): - app = Sanic('test_route_duplicate') +def test_route_duplicate(app): with pytest.raises(RouteExists): @app.route('/test') @@ -570,8 +538,7 @@ def test_route_duplicate(): pass -def test_method_not_allowed(): - app = Sanic('test_method_not_allowed') +def test_method_not_allowed(app): @app.route('/test', methods=['GET']) async def handler(request): @@ -584,8 +551,7 @@ def test_method_not_allowed(): assert response.status == 405 -def test_static_add_route(): - app = Sanic('test_static_add_route') +def test_static_add_route(app): async def handler1(request): return text('OK1') @@ -603,8 +569,7 @@ def test_static_add_route(): assert response.text == 'OK2' -def test_dynamic_add_route(): - app = Sanic('test_dynamic_add_route') +def test_dynamic_add_route(app): results = [] @@ -619,8 +584,7 @@ def test_dynamic_add_route(): assert results[0] == 'test123' -def test_dynamic_add_route_string(): - app = Sanic('test_dynamic_add_route_string') +def test_dynamic_add_route_string(app): results = [] @@ -640,9 +604,7 @@ def test_dynamic_add_route_string(): assert results[1] == 'favicon.ico' -def test_dynamic_add_route_int(): - app = Sanic('test_dynamic_add_route_int') - +def test_dynamic_add_route_int(app): results = [] async def handler(request, folder_id): @@ -659,9 +621,7 @@ def test_dynamic_add_route_int(): assert response.status == 404 -def test_dynamic_add_route_number(): - app = Sanic('test_dynamic_add_route_number') - +def test_dynamic_add_route_number(app): results = [] async def handler(request, weight): @@ -681,8 +641,7 @@ def test_dynamic_add_route_number(): assert response.status == 404 -def test_dynamic_add_route_regex(): - app = Sanic('test_dynamic_route_int') +def test_dynamic_add_route_regex(app): async def handler(request, folder_id): return text('OK') @@ -702,8 +661,7 @@ def test_dynamic_add_route_regex(): assert response.status == 200 -def test_dynamic_add_route_unhashable(): - app = Sanic('test_dynamic_add_route_unhashable') +def test_dynamic_add_route_unhashable(app): async def handler(request, unhashable): return text('OK') @@ -723,8 +681,7 @@ def test_dynamic_add_route_unhashable(): assert response.status == 404 -def test_add_route_duplicate(): - app = Sanic('test_add_route_duplicate') +def test_add_route_duplicate(app): with pytest.raises(RouteExists): async def handler1(request): @@ -747,8 +704,7 @@ def test_add_route_duplicate(): app.add_route(handler2, '/test//') -def test_add_route_method_not_allowed(): - app = Sanic('test_add_route_method_not_allowed') +def test_add_route_method_not_allowed(app): async def handler(request): return text('OK') @@ -762,8 +718,7 @@ def test_add_route_method_not_allowed(): assert response.status == 405 -def test_remove_static_route(): - app = Sanic('test_remove_static_route') +def test_remove_static_route(app): async def handler1(request): return text('OK1') @@ -790,8 +745,7 @@ def test_remove_static_route(): assert response.status == 404 -def test_remove_dynamic_route(): - app = Sanic('test_remove_dynamic_route') +def test_remove_dynamic_route(app): async def handler(request, name): return text('OK') @@ -806,15 +760,13 @@ def test_remove_dynamic_route(): assert response.status == 404 -def test_remove_inexistent_route(): - app = Sanic('test_remove_inexistent_route') +def test_remove_inexistent_route(app): with pytest.raises(RouteDoesNotExist): app.remove_route('/test') -def test_removing_slash(): - app = Sanic(__name__) +def test_removing_slash(app): @app.get('/rest/') def get(_): @@ -827,8 +779,7 @@ def test_removing_slash(): assert len(app.router.routes_all.keys()) == 2 -def test_remove_unhashable_route(): - app = Sanic('test_remove_unhashable_route') +def test_remove_unhashable_route(app): async def handler(request, unhashable): return text('OK') @@ -856,8 +807,7 @@ def test_remove_unhashable_route(): assert response.status == 404 -def test_remove_route_without_clean_cache(): - app = Sanic('test_remove_static_route') +def test_remove_route_without_clean_cache(app): async def handler(request): return text('OK') @@ -884,8 +834,7 @@ def test_remove_route_without_clean_cache(): assert response.status == 200 -def test_overload_routes(): - app = Sanic('test_dynamic_route') +def test_overload_routes(app): @app.route('/overload', methods=['GET']) async def handler1(request): @@ -913,8 +862,7 @@ def test_overload_routes(): return text('Duplicated') -def test_unmergeable_overload_routes(): - app = Sanic('test_dynamic_route') +def test_unmergeable_overload_routes(app): @app.route('/overload_whole', methods=None) async def handler1(request): @@ -947,8 +895,7 @@ def test_unmergeable_overload_routes(): assert response.status == 405 -def test_unicode_routes(): - app = Sanic('test_unicode_routes') +def test_unicode_routes(app): @app.get('/你好') def handler1(request): @@ -965,8 +912,7 @@ def test_unicode_routes(): assert response.text == 'OK2 你好' -def test_uri_with_different_method_and_different_params(): - app = Sanic('test_uri') +def test_uri_with_different_method_and_different_params(app): @app.route('/ads/', methods=['GET']) async def ad_get(request, ad_id): diff --git a/tests/test_server_events.py b/tests/test_server_events.py index c2ccc7dc..68e097eb 100644 --- a/tests/test_server_events.py +++ b/tests/test_server_events.py @@ -1,11 +1,7 @@ -from io import StringIO -from random import choice -from string import ascii_letters import signal import pytest -from sanic import Sanic from sanic.testing import HOST, PORT AVAILABLE_LISTENERS = [ @@ -37,54 +33,46 @@ def start_stop_app(random_name_app, **run_kwargs): @pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS) -def test_single_listener(listener_name): +def test_single_listener(app, listener_name): """Test that listeners on their own work""" - random_name_app = Sanic(''.join( - [choice(ascii_letters) for _ in range(choice(range(5, 10)))])) - output = list() + output = [] # Register listener - random_name_app.listener(listener_name)( + app.listener(listener_name)( create_listener(listener_name, output)) - start_stop_app(random_name_app) - assert random_name_app.name + listener_name == output.pop() + start_stop_app(app) + assert app.name + listener_name == output.pop() @pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS) -def test_register_listener(listener_name): +def test_register_listener(app, listener_name): """ Test that listeners on their own work with app.register_listener method """ - random_name_app = Sanic(''.join( - [choice(ascii_letters) for _ in range(choice(range(5, 10)))])) - output = list() + output = [] # Register listener listener = create_listener(listener_name, output) - random_name_app.register_listener(listener, + app.register_listener(listener, event=listener_name) - start_stop_app(random_name_app) - assert random_name_app.name + listener_name == output.pop() + start_stop_app(app) + assert app.name + listener_name == output.pop() -def test_all_listeners(): - random_name_app = Sanic(''.join( - [choice(ascii_letters) for _ in range(choice(range(5, 10)))])) - output = list() +def test_all_listeners(app): + output = [] for listener_name in AVAILABLE_LISTENERS: listener = create_listener(listener_name, output) - random_name_app.listener(listener_name)(listener) - start_stop_app(random_name_app) + app.listener(listener_name)(listener) + start_stop_app(app) for listener_name in AVAILABLE_LISTENERS: - assert random_name_app.name + listener_name == output.pop() + assert app.name + listener_name == output.pop() -async def test_trigger_before_events_create_server(): +async def test_trigger_before_events_create_server(app): class MySanicDb: pass - app = Sanic("test_sanic_app") - @app.listener('before_server_start') async def init_db(app, loop): app.db = MySanicDb() diff --git a/tests/test_signal_handlers.py b/tests/test_signal_handlers.py index bee4f8e7..7c1327c2 100644 --- a/tests/test_signal_handlers.py +++ b/tests/test_signal_handlers.py @@ -1,4 +1,3 @@ -from sanic import Sanic from sanic.response import HTTPResponse from sanic.testing import HOST, PORT from unittest.mock import MagicMock @@ -18,9 +17,8 @@ def set_loop(app, loop): def after(app, loop): calledq.put(loop.add_signal_handler.called) -def test_register_system_signals(): +def test_register_system_signals(app): """Test if sanic register system signals""" - app = Sanic('test_register_system_signals') @app.route('/hello') async def hello_route(request): @@ -34,9 +32,8 @@ def test_register_system_signals(): assert calledq.get() == True -def test_dont_register_system_signals(): +def test_dont_register_system_signals(app): """Test if sanic don't register system signals""" - app = Sanic('test_register_system_signals') @app.route('/hello') async def hello_route(request): diff --git a/tests/test_static.py b/tests/test_static.py index 3335e248..e436aa74 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -3,8 +3,6 @@ import os import pytest -from sanic import Sanic - @pytest.fixture(scope='module') def static_file_directory(): @@ -26,8 +24,7 @@ def get_file_content(static_file_directory, file_name): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) -def test_static_file(static_file_directory, file_name): - app = Sanic('test_static') +def test_static_file(app, static_file_directory, file_name): app.static( '/testing.file', get_file_path(static_file_directory, file_name)) @@ -37,8 +34,7 @@ def test_static_file(static_file_directory, file_name): @pytest.mark.parametrize('file_name', ['test.html']) -def test_static_file_content_type(static_file_directory, file_name): - app = Sanic('test_static') +def test_static_file_content_type(app, static_file_directory, file_name): app.static( '/testing.file', get_file_path(static_file_directory, file_name), @@ -53,9 +49,7 @@ def test_static_file_content_type(static_file_directory, file_name): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) @pytest.mark.parametrize('base_uri', ['/static', '', '/dir']) -def test_static_directory(file_name, base_uri, static_file_directory): - - app = Sanic('test_static') +def test_static_directory(app, file_name, base_uri, static_file_directory): app.static(base_uri, static_file_directory) request, response = app.test_client.get( @@ -65,8 +59,7 @@ def test_static_directory(file_name, base_uri, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_head_request(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_head_request(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -81,8 +74,7 @@ def test_static_head_request(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_correct(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_correct(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -102,8 +94,7 @@ def test_static_content_range_correct(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_front(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_front(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -123,8 +114,7 @@ def test_static_content_range_front(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_back(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_back(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -144,8 +134,7 @@ def test_static_content_range_back(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_empty(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_empty(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -161,8 +150,7 @@ def test_static_content_range_empty(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_error(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_error(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -179,8 +167,7 @@ def test_static_content_range_error(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) -def test_static_file_specified_host(static_file_directory, file_name): - app = Sanic('test_static') +def test_static_file_specified_host(app, static_file_directory, file_name): app.static( '/testing.file', get_file_path(static_file_directory, file_name), diff --git a/tests/test_url_building.py b/tests/test_url_building.py index 98bbc20a..1768f78f 100644 --- a/tests/test_url_building.py +++ b/tests/test_url_building.py @@ -1,7 +1,6 @@ import pytest as pytest from urllib.parse import urlsplit, parse_qsl -from sanic import Sanic from sanic.response import text from sanic.views import HTTPMethodView from sanic.blueprints import Blueprint @@ -30,8 +29,7 @@ def _generate_handlers_from_names(app, l): @pytest.fixture -def simple_app(): - app = Sanic('simple_app') +def simple_app(app): handler_names = list(string.ascii_letters) _generate_handlers_from_names(app, handler_names) @@ -54,8 +52,7 @@ def test_simple_url_for_getting(simple_app): (URL_FOR_ARGS2, URL_FOR_VALUE2), (URL_FOR_ARGS3, URL_FOR_VALUE3), (URL_FOR_ARGS4, URL_FOR_VALUE4)]) -def test_simple_url_for_getting_with_more_params(args, url): - app = Sanic('more_url_build') +def test_simple_url_for_getting_with_more_params(app, args, url): @app.route('/myurl') def passes(request): @@ -67,8 +64,7 @@ def test_simple_url_for_getting_with_more_params(args, url): assert response.text == 'this should pass' -def test_fails_if_endpoint_not_found(): - app = Sanic('fail_url_build') +def test_fails_if_endpoint_not_found(app): @app.route('/fail') def fail(request): @@ -80,14 +76,12 @@ def test_fails_if_endpoint_not_found(): assert str(e.value) == 'Endpoint with name `passes` was not found' -def test_fails_url_build_if_param_not_passed(): +def test_fails_url_build_if_param_not_passed(app): url = '/' for letter in string.ascii_letters: url += '<{}>/'.format(letter) - app = Sanic('fail_url_build') - @app.route(url) def fail(request): return text('this should fail') @@ -103,8 +97,7 @@ def test_fails_url_build_if_param_not_passed(): assert 'Required parameter `Z` was not passed to url_for' in str(e.value) -def test_fails_url_build_if_params_not_passed(): - app = Sanic('fail_url_build') +def test_fails_url_build_if_params_not_passed(app): @app.route('/fail') def fail(request): @@ -126,8 +119,7 @@ PASSING_KWARGS = { EXPECTED_BUILT_URL = '/4/woof/ba/normal/1.001' -def test_fails_with_int_message(): - app = Sanic('fail_url_build') +def test_fails_with_int_message(app): @app.route(COMPLEX_PARAM_URL) def fail(request): @@ -145,8 +137,7 @@ def test_fails_with_int_message(): assert str(e.value) == expected_error -def test_fails_with_two_letter_string_message(): - app = Sanic('fail_url_build') +def test_fails_with_two_letter_string_message(app): @app.route(COMPLEX_PARAM_URL) def fail(request): @@ -165,8 +156,7 @@ def test_fails_with_two_letter_string_message(): assert str(e.value) == expected_error -def test_fails_with_number_message(): - app = Sanic('fail_url_build') +def test_fails_with_number_message(app): @app.route(COMPLEX_PARAM_URL) def fail(request): @@ -185,8 +175,7 @@ def test_fails_with_number_message(): assert str(e.value) == expected_error -def test_adds_other_supplied_values_as_query_string(): - app = Sanic('passes') +def test_adds_other_supplied_values_as_query_string(app): @app.route(COMPLEX_PARAM_URL) def passes(request): @@ -205,8 +194,7 @@ def test_adds_other_supplied_values_as_query_string(): @pytest.fixture -def blueprint_app(): - app = Sanic('blueprints') +def blueprint_app(app): first_print = Blueprint('first', url_prefix='/first') second_print = Blueprint('second', url_prefix='/second') @@ -252,8 +240,7 @@ def test_blueprints_work_with_params(blueprint_app): @pytest.fixture -def methodview_app(): - app = Sanic('methodview') +def methodview_app(app): class ViewOne(HTTPMethodView): def get(self, request): diff --git a/tests/test_url_for_static.py b/tests/test_url_for_static.py index d1d8fc9b..2e802700 100644 --- a/tests/test_url_for_static.py +++ b/tests/test_url_for_static.py @@ -3,7 +3,6 @@ import os import pytest -from sanic import Sanic from sanic.blueprints import Blueprint @@ -27,8 +26,7 @@ def get_file_content(static_file_directory, file_name): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) -def test_static_file(static_file_directory, file_name): - app = Sanic('test_static') +def test_static_file(app, static_file_directory, file_name): app.static( '/testing.file', get_file_path(static_file_directory, file_name)) app.static( @@ -102,9 +100,7 @@ def test_static_file(static_file_directory, file_name): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) @pytest.mark.parametrize('base_uri', ['/static', '', '/dir']) -def test_static_directory(file_name, base_uri, static_file_directory): - - app = Sanic('test_static') +def test_static_directory(app, file_name, base_uri, static_file_directory): app.static(base_uri, static_file_directory) base_uri2 = base_uri + '/2' app.static(base_uri2, static_file_directory, name='uploads') @@ -156,10 +152,8 @@ def test_static_directory(file_name, base_uri, static_file_directory): assert response.body == get_file_content(static_file_directory, file_name) - @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_head_request(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_head_request(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -198,8 +192,7 @@ def test_static_head_request(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_correct(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_correct(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -250,8 +243,7 @@ def test_static_content_range_correct(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_front(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_front(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -302,8 +294,7 @@ def test_static_content_range_front(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_back(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_back(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -354,8 +345,7 @@ def test_static_content_range_back(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_empty(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_empty(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -401,8 +391,7 @@ def test_static_content_range_empty(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_error(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_error(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) diff --git a/tests/test_utf8.py b/tests/test_utf8.py index e1602958..4a8b9e73 100644 --- a/tests/test_utf8.py +++ b/tests/test_utf8.py @@ -1,14 +1,12 @@ -from json import loads as json_loads, dumps as json_dumps -from sanic import Sanic -from sanic.response import json, text +from json import dumps as json_dumps +from sanic.response import text # ------------------------------------------------------------ # # UTF-8 # ------------------------------------------------------------ # -def test_utf8_query_string(): - app = Sanic('test_utf8_query_string') +def test_utf8_query_string(app): @app.route('/') async def handler(request): @@ -18,8 +16,7 @@ def test_utf8_query_string(): assert request.args.get('utf8') == '✓' -def test_utf8_response(): - app = Sanic('test_utf8_response') +def test_utf8_response(app): @app.route('/') async def handler(request): @@ -29,8 +26,7 @@ def test_utf8_response(): assert response.text == '✓' -def skip_test_utf8_route(): - app = Sanic('skip_test_utf8_route') +def skip_test_utf8_route(app): @app.route('/') async def handler(request): @@ -41,8 +37,7 @@ def skip_test_utf8_route(): assert response.text == 'OK' -def test_utf8_post_json(): - app = Sanic('test_utf8_post_json') +def test_utf8_post_json(app): @app.route('/') async def handler(request): diff --git a/tests/test_vhosts.py b/tests/test_vhosts.py index 2b88bff3..0be537e6 100644 --- a/tests/test_vhosts.py +++ b/tests/test_vhosts.py @@ -1,9 +1,7 @@ -from sanic import Sanic -from sanic.response import json, text +from sanic.response import text -def test_vhosts(): - app = Sanic('test_vhosts') +def test_vhosts(app): @app.route('/', host="example.com") async def handler(request): @@ -22,8 +20,7 @@ def test_vhosts(): assert response.text == "You're at subdomain.example.com!" -def test_vhosts_with_list(): - app = Sanic('test_vhosts') +def test_vhosts_with_list(app): @app.route('/', host=["hello.com", "world.com"]) async def handler(request): @@ -37,8 +34,8 @@ def test_vhosts_with_list(): request, response = app.test_client.get('/', headers=headers) assert response.text == "Hello, world!" -def test_vhosts_with_defaults(): - app = Sanic('test_vhosts') + +def test_vhosts_with_defaults(app): @app.route('/', host="hello.com") async def handler(request): diff --git a/tests/test_views.py b/tests/test_views.py index 71d32a7f..5d339f55 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,6 +1,5 @@ import pytest as pytest -from sanic import Sanic from sanic.exceptions import InvalidUsage from sanic.response import text, HTTPResponse from sanic.views import HTTPMethodView, CompositionView @@ -10,8 +9,7 @@ from sanic.constants import HTTP_METHODS @pytest.mark.parametrize('method', HTTP_METHODS) -def test_methods(method): - app = Sanic('test_methods') +def test_methods(app, method): class DummyView(HTTPMethodView): @@ -44,8 +42,7 @@ def test_methods(method): assert response.headers['method'] == method -def test_unexisting_methods(): - app = Sanic('test_unexisting_methods') +def test_unexisting_methods(app): class DummyView(HTTPMethodView): @@ -59,8 +56,7 @@ def test_unexisting_methods(): assert response.text == 'Error: Method POST not allowed for URL /' -def test_argument_methods(): - app = Sanic('test_argument_methods') +def test_argument_methods(app): class DummyView(HTTPMethodView): @@ -74,8 +70,7 @@ def test_argument_methods(): assert response.text == 'I am get method with test123' -def test_with_bp(): - app = Sanic('test_with_bp') +def test_with_bp(app): bp = Blueprint('test_text') class DummyView(HTTPMethodView): @@ -93,8 +88,7 @@ def test_with_bp(): assert response.text == 'I am get method' -def test_with_bp_with_url_prefix(): - app = Sanic('test_with_bp_with_url_prefix') +def test_with_bp_with_url_prefix(app): bp = Blueprint('test_text', url_prefix='/test1') class DummyView(HTTPMethodView): @@ -110,8 +104,7 @@ def test_with_bp_with_url_prefix(): assert response.text == 'I am get method' -def test_with_middleware(): - app = Sanic('test_with_middleware') +def test_with_middleware(app): class DummyView(HTTPMethodView): @@ -132,9 +125,7 @@ def test_with_middleware(): assert type(results[0]) is Request -def test_with_middleware_response(): - app = Sanic('test_with_middleware_response') - +def test_with_middleware_response(app): results = [] @app.middleware('request') @@ -161,8 +152,7 @@ def test_with_middleware_response(): assert isinstance(results[2], HTTPResponse) -def test_with_custom_class_methods(): - app = Sanic('test_with_custom_class_methods') +def test_with_custom_class_methods(app): class DummyView(HTTPMethodView): global_var = 0 @@ -179,9 +169,7 @@ def test_with_custom_class_methods(): assert response.text == 'I am get method and global var is 10' -def test_with_decorator(): - app = Sanic('test_with_decorator') - +def test_with_decorator(app): results = [] def stupid_decorator(view): @@ -227,9 +215,7 @@ def test_composition_view_rejects_duplicate_methods(): @pytest.mark.parametrize('method', HTTP_METHODS) -def test_composition_view_runs_methods_as_expected(method): - app = Sanic('test_composition_view') - +def test_composition_view_runs_methods_as_expected(app, method): view = CompositionView() def first(request): @@ -251,9 +237,7 @@ def test_composition_view_runs_methods_as_expected(method): @pytest.mark.parametrize('method', HTTP_METHODS) -def test_composition_view_rejects_invalid_methods(method): - app = Sanic('test_composition_view') - +def test_composition_view_rejects_invalid_methods(app, method): view = CompositionView() view.add(['GET', 'POST', 'PUT'], lambda x: text('first method')) From c57897424689df434d7faeb42ed928744eb02644 Mon Sep 17 00:00:00 2001 From: Igor Gnatenko Date: Sun, 2 Sep 2018 09:19:19 +0200 Subject: [PATCH 18/32] Switch to websockets 6.0 Signed-off-by: Igor Gnatenko --- environment.yml | 2 +- requirements.txt | 2 +- sanic/websocket.py | 16 +++++----------- setup.py | 2 +- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/environment.yml b/environment.yml index 1c1dd82f..20cd1509 100644 --- a/environment.yml +++ b/environment.yml @@ -15,6 +15,6 @@ dependencies: - httptools>=0.0.9 - ujson>=1.35 - aiofiles>=0.3.0 - - websockets>=3.2 + - websockets>=6.0 - sphinxcontrib-asyncio>=0.2.0 - https://github.com/channelcat/docutils-fork/zipball/master diff --git a/requirements.txt b/requirements.txt index e320e781..4404d5d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ aiofiles httptools ujson; sys_platform != "win32" and implementation_name == "cpython" uvloop; sys_platform != "win32" and implementation_name == "cpython" -websockets>=5.0,<6.0 +websockets>=6.0,<7.0 multidict>=4.0,<5.0 diff --git a/sanic/websocket.py b/sanic/websocket.py index 99408af5..9ccf9fdf 100644 --- a/sanic/websocket.py +++ b/sanic/websocket.py @@ -57,17 +57,11 @@ class WebSocketProtocol(HttpProtocol): async def websocket_handshake(self, request, subprotocols=None): # let the websockets package do the handshake with the client - headers = [] - - def get_header(k): - return request.headers.get(k, '') - - def set_header(k, v): - headers.append((k, v)) + headers = {} try: - key = handshake.check_request(get_header) - handshake.build_response(set_header, key) + key = handshake.check_request(request.headers) + handshake.build_response(headers, key) except InvalidHandshake: raise InvalidUsage('Invalid websocket request') @@ -79,12 +73,12 @@ class WebSocketProtocol(HttpProtocol): for p in client_subprotocols: if p in subprotocols: subprotocol = p - set_header('Sec-Websocket-Protocol', subprotocol) + headers['Sec-Websocket-Protocol'] = subprotocol break # write the 101 response back to the client rv = b'HTTP/1.1 101 Switching Protocols\r\n' - for k, v in headers: + for k, v in headers.items(): rv += k.encode('utf-8') + b': ' + v.encode('utf-8') + b'\r\n' rv += b'\r\n' request.transport.write(rv) diff --git a/setup.py b/setup.py index 34703ab4..7df2964f 100644 --- a/setup.py +++ b/setup.py @@ -60,7 +60,7 @@ requirements = [ uvloop, ujson, 'aiofiles>=0.3.0', - 'websockets>=5.0,<6.0', + 'websockets>=6.0,<7.0', 'multidict>=4.0,<5.0', ] if strtobool(os.environ.get("SANIC_NO_UJSON", "no")): From 9082eb56a73d8bf2715fa9a46841567f9ac8173c Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Thu, 6 Sep 2018 13:51:31 -0700 Subject: [PATCH 19/32] Update version to circumvent pypi upload errors --- sanic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index 5e6ff4da..e675195a 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from sanic.app import Sanic from sanic.blueprints import Blueprint -__version__ = '0.8.0' +__version__ = '0.8.1' __all__ = ['Sanic', 'Blueprint'] From 7ae0eb0dc3c28a459ee757c0fa6f17a0921b2ab5 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Thu, 13 Sep 2018 01:39:24 -0700 Subject: [PATCH 20/32] Transfer ownership --- README.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 01801ddd..19b0a9f8 100644 --- a/README.rst +++ b/README.rst @@ -7,9 +7,9 @@ Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's ba On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy. -Sanic is developed `on GitHub `_. Contributions are welcome! +Sanic is developed `on GitHub `_. Contributions are welcome! -If you have a project that utilizes Sanic make sure to comment on the `issue `_ that we use to track those projects! +If you have a project that utilizes Sanic make sure to comment on the `issue `_ that we use to track those projects! Hello World Example ------------------- @@ -47,8 +47,8 @@ Documentation .. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg :target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge -.. |Build Status| image:: https://travis-ci.org/channelcat/sanic.svg?branch=master - :target: https://travis-ci.org/channelcat/sanic +.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master + :target: https://travis-ci.org/huge-success/sanic .. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest :target: http://sanic.readthedocs.io/en/latest/?badge=latest .. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg @@ -59,11 +59,11 @@ Documentation Examples -------- -`Non-Core examples `_. Examples of plugins and Sanic that are outside the scope of Sanic core. +`Non-Core examples `_. Examples of plugins and Sanic that are outside the scope of Sanic core. -`Extensions `_. Sanic extensions created by the community. +`Extensions `_. Sanic extensions created by the community. -`Projects `_. Sanic in production use. +`Projects `_. Sanic in production use. TODO From d38fc1719127851afad9be18b94dc845b5049f01 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Thu, 13 Sep 2018 01:50:32 -0700 Subject: [PATCH 21/32] Update version to test pypi --- sanic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index e675195a..912a151c 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from sanic.app import Sanic from sanic.blueprints import Blueprint -__version__ = '0.8.1' +__version__ = '0.8.2' __all__ = ['Sanic', 'Blueprint'] From 3e616b599a59c91bbaeef17145c400f96be9474d Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Thu, 13 Sep 2018 02:17:27 -0700 Subject: [PATCH 22/32] update encrypted creds for new org --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c18e895b..83f13caf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,7 @@ deploy: provider: pypi user: channelcat password: - secure: OgADRQH3+dTL5swGzXkeRJDNbLpFzwqYnXB4iLD0Npvzj9QnKyQVvkbaeq6VmV9dpEFb5ULaAKYQq19CrXYDm28yanUSn6jdJ4SukaHusi7xt07U6H7pmoX/uZ2WZYqCSLM8cSp8TXY/3oV3rY5Jfj/AibE5XTbim5/lrhsvW6NR+ALzxc0URRPAHDZEPpojTCjSTjpY0aDsaKWg4mXVRMFfY3O68j6KaIoukIZLuoHfePLKrbZxaPG5VxNhMHEaICdxVxE/dO+7pQmQxXuIsEOHK1QiVJ9YrSGcNqgEqhN36kYP8dqMeVB07sv8Xa6o/Uax2/wXS2HEJvuwP1YD6WkoZuo9ZB85bcMdg7BV9jJDbVFVPJwc75BnTLHrMa3Q1KrRlKRDBUXBUsQivPuWhFNwUgvEayq2qSI3aRQR4Z0O+DfboEhXYojSoD64/EWBTZ7vhgbvOTGEdukUQSYrKj9P8jc1s8exomTsAiqdFxTUpzfiammUSL+M93lP4urtahl1jjXFX7gd3DzdEEb0NsGkx5lm/qdsty8/TeAvKUmC+RVU6T856W6MqN0P+yGbpWUARcSE7fwztC3SPxwAuxvIN3BHmRhOUHoORPNG2VpfbnscIzBKJR4v0JKzbpi0IDa66K+tCGsCEvQuL4cxVOtoUySPWNSUAyUWWUrGM2k= + secure: h7oNDjA/ObDBGK7xt55SV0INHOclMJW/byxMrNxvCZ0JxiRk7WBNtWYt0WJjyf5lO/L0/sfgiAk0GIdFon57S24njSLPAq/a4ptkWZ68s2A+TaF6ezJSZvE9V8khivjoeub90TzfX6P5aukRja1CSxXKJm+v0V8hGE4CZGyCgEDvK3JqIakpXllSDl19DhVftCS/lQZD7AXrZlg1kZnPCMtB5IbCVR4L2bfrSJVNptBi2CqqxacY2MOLu+jv5FzJ2BGVIJ2zoIJS2T+JmGJzpiamF6y8Amv0667i9lg2DXWCtI3PsQzCmwa3F/ZsI+ohUAvJC5yvzP7SyTJyXifRBdJ9O137QkNAHFoJOOY3B4GSnTo8/boajKXEqGiV4h2EgwNjBaR0WJl0pB7HHUCBMkNRWqo6ACB8eCr04tXWXPvkGIc+wPjq960hsUZea1O31MuktYc9Ot6eiFqm7OKoItdi7LxCen1eTj93ePgkiEnVZ+p/04Hh1U7CX31UJMNu5kCvZPIANnAuDsS2SK7Qkr88OAuWL0wmrBcXKOcnVkJtZ5mzx8T54bI1RrSYtFDBLFfOPb0GucSziMBtQpE76qPEauVwIXBk3RnR8N57xBR/lvTaIk758tf+haO0llEO5rVls1zLNZ+VlTzXy7hX0OZbdopIAcCFBFWqWMAdXQc= on: tags: true distributions: "sdist bdist_wheel" From d8f9986089311c9fe07e1970f39043cf211af79a Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Thu, 13 Sep 2018 02:24:31 -0700 Subject: [PATCH 23/32] Re-releasing with updated credentials --- sanic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index 912a151c..51c8268e 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from sanic.app import Sanic from sanic.blueprints import Blueprint -__version__ = '0.8.2' +__version__ = '0.8.3' __all__ = ['Sanic', 'Blueprint'] From bb35bc3898101783e07897d43c16c838cb0d44b1 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 14 Sep 2018 16:00:29 +0200 Subject: [PATCH 24/32] Add multidict to readthedocs environment.yml Signed-off-by: Eli Uriegas --- environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/environment.yml b/environment.yml index 1c1dd82f..9f416c0e 100644 --- a/environment.yml +++ b/environment.yml @@ -17,4 +17,5 @@ dependencies: - aiofiles>=0.3.0 - websockets>=3.2 - sphinxcontrib-asyncio>=0.2.0 + - multidict>=4.0<5.0 - https://github.com/channelcat/docutils-fork/zipball/master From 78efcf93f8ece3fb9695b6cd15b1811fe7821e78 Mon Sep 17 00:00:00 2001 From: Stephen Sadowski Date: Fri, 14 Sep 2018 10:56:32 -0500 Subject: [PATCH 25/32] Updated changelog for all accepted PRs from 0.7.0 to Current --- CHANGELOG.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84e7be78..4d7ecb54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,80 @@ +Version 0.8 +----------- +- 0.8.3 + - Changes: + - Ownership changed to org 'huge-success' + +- 0.8.0 + - Changes: + - Add Server-Sent Events extension (Innokenty Lebedev) + - Graceful handling of request_handler_task cancellation (Ashley Sommer) + - Sanitize URL before redirection (aveao) + - Add url_bytes to request (johndoe46) + - py37 support for travisci (yunstanford) + - Auto reloader support for OSX (garyo) + - Add UUID route support (Volodymyr Maksymiv) + - Add pausable response streams (Ashley Sommer) + - Add weakref to request slots (vopankov) + - remove ubuntu 12.04 from test fixture due to deprecation (yunstanford) + - Allow streaming handlers in add_route (kinware) + - use travis_retry for tox (Raphael Deems) + - update aiohttp version for test client (yunstanford) + - add redirect import for clarity (yingshaoxo) + - Update HTTP Entity headers (Arnulfo Solís) + - Add register_listener method (Stephan Fitzpatrick) + - Remove uvloop/ujson dependencies for Windows (abuckenheimer) + - Content-length header on 204/304 responses (Arnulfo Solís) + - Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford) + - Update development status from pre-alpha to beta (Maksim Anisenkov) + - KeepAlive Timout log level changed to debug (Arnulfo Solís) + - Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov) + - Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad) + - Add support for blueprint groups and nesting (Elias Tarhini) + - Remove uvloop for windows setup (Aleksandr Kurlov) + - Auto Reload (Yaser Amari) + - Documentation updates/fixups (multiple contributors) + + - Fixes: + - Fix: auto_reload in Linux (Ashley Sommer) + - Fix: broken tests for aiohttp >= 3.3.0 (Ashley Sommer) + - Fix: disable auto_reload by default on windows (abuckenheimer) + - Fix (1143): Turn off access log with gunicorn (hqy) + - Fix (1268): Support status code for file response (Cosmo Borsky) + - Fix (1266): Add content_type flag to Sanic.static (Cosmo Borsky) + - Fix: subprotocols parameter missing from add_websocket_route (ciscorn) + - Fix (1242): Responses for CI header (yunstanford) + - Fix (1237): add version constraint for websockets (yunstanford) + - Fix (1231): memory leak - always release resource (Phillip Xu) + - Fix (1221): make request truthy if transport exists (Raphael Deems) + - Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer) + - Fix try_everything examples (PyManiacGR, kot83) + - Fix (1158): default to auto_reload in debug mode (Raphael Deems) + - Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux) + - Fix: raw requires bytes-like object (cloudship) + - Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe) + - Fix: Bug in multipart/form-data parser (DirkGuijt) + - Fix: Exception for missing parameter when value is null (NyanKiyoshi) + - Fix: Parameter check (Howie Hu) + - Fix (1089): Routing issue with named parameters and different methods (yunstanford) + - Fix (1085): Signal handling in multi-worker mode (yunstanford) + - Fix: single quote in readme.rst (Cosven) + - Fix: method typos (Dmitry Dygalo) + - Fix: log_response correct output for ip and port (Wibowo Arindrarto) + - Fix (1042): Exception Handling (Raphael Deems) + - Fix: Chinese URIs (Howie Hu) + - Fix (1079): timeout bug when self.transport is None (Raphael Deems) + - Fix (1074): fix strict_slashes when route has slash (Raphael Deems) + - Fix (1050): add samesite cookie to cookie keys (Raphael Deems) + - Fix (1065): allow add_task after server starts (Raphael Deems) + - Fix (1061): double quotes in unauthorized exception (Raphael Deems) + - Fix (1062): inject the app in add_task method (Raphael Deems) + - Fix: update environment.yml for readthedocs (Eli Uriegas) + - Fix: Cancel request task when response timeout is triggered (Jeong YunWon) + - Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deems) + - Fix: IPv6 Address and Socket Data Format (Dan Palmer) + +Note: Changelog was unmaintained between 0.1 and 0.7 + Version 0.1 ----------- - 0.1.7 @@ -5,18 +82,18 @@ Version 0.1 - 0.1.6 - Static files - Lazy Cookie Loading - - 0.1.5 + - 0.1.5 - Cookies - Blueprint listeners and ordering - Faster Router - Fix: Incomplete file reads on medium+ sized post requests - Breaking: after_start and before_stop now pass sanic as their first argument - - 0.1.4 + - 0.1.4 - Multiprocessing - 0.1.3 - Blueprint support - Faster Response processing - - 0.1.1 - 0.1.2 + - 0.1.1 - 0.1.2 - Struggling to update pypi via CI - - 0.1.0 - - Released to public \ No newline at end of file + - 0.1.0 + - Released to public From 5851c8bd91be858cde82b3aba9425c2600153e1a Mon Sep 17 00:00:00 2001 From: Stephen Sadowski Date: Fri, 14 Sep 2018 13:30:57 -0500 Subject: [PATCH 26/32] revised formatting for CHANGELOG.md --- CHANGELOG.md | 140 +++++++++++++++++++++++++-------------------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d7ecb54..cbff316c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,77 +1,77 @@ Version 0.8 ----------- -- 0.8.3 - - Changes: - - Ownership changed to org 'huge-success' +0.8.3 + - Changes: + - Ownership changed to org 'huge-success' -- 0.8.0 - - Changes: - - Add Server-Sent Events extension (Innokenty Lebedev) - - Graceful handling of request_handler_task cancellation (Ashley Sommer) - - Sanitize URL before redirection (aveao) - - Add url_bytes to request (johndoe46) - - py37 support for travisci (yunstanford) - - Auto reloader support for OSX (garyo) - - Add UUID route support (Volodymyr Maksymiv) - - Add pausable response streams (Ashley Sommer) - - Add weakref to request slots (vopankov) - - remove ubuntu 12.04 from test fixture due to deprecation (yunstanford) - - Allow streaming handlers in add_route (kinware) - - use travis_retry for tox (Raphael Deems) - - update aiohttp version for test client (yunstanford) - - add redirect import for clarity (yingshaoxo) - - Update HTTP Entity headers (Arnulfo Solís) - - Add register_listener method (Stephan Fitzpatrick) - - Remove uvloop/ujson dependencies for Windows (abuckenheimer) - - Content-length header on 204/304 responses (Arnulfo Solís) - - Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford) - - Update development status from pre-alpha to beta (Maksim Anisenkov) - - KeepAlive Timout log level changed to debug (Arnulfo Solís) - - Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov) - - Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad) - - Add support for blueprint groups and nesting (Elias Tarhini) - - Remove uvloop for windows setup (Aleksandr Kurlov) - - Auto Reload (Yaser Amari) - - Documentation updates/fixups (multiple contributors) +0.8.0 + - Changes: + - Add Server-Sent Events extension (Innokenty Lebedev) + - Graceful handling of request_handler_task cancellation (Ashley Sommer) + - Sanitize URL before redirection (aveao) + - Add url_bytes to request (johndoe46) + - py37 support for travisci (yunstanford) + - Auto reloader support for OSX (garyo) + - Add UUID route support (Volodymyr Maksymiv) + - Add pausable response streams (Ashley Sommer) + - Add weakref to request slots (vopankov) + - remove ubuntu 12.04 from test fixture due to deprecation (yunstanford) + - Allow streaming handlers in add_route (kinware) + - use travis_retry for tox (Raphael Deems) + - update aiohttp version for test client (yunstanford) + - add redirect import for clarity (yingshaoxo) + - Update HTTP Entity headers (Arnulfo Solís) + - Add register_listener method (Stephan Fitzpatrick) + - Remove uvloop/ujson dependencies for Windows (abuckenheimer) + - Content-length header on 204/304 responses (Arnulfo Solís) + - Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford) + - Update development status from pre-alpha to beta (Maksim Anisenkov) + - KeepAlive Timout log level changed to debug (Arnulfo Solís) + - Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov) + - Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad) + - Add support for blueprint groups and nesting (Elias Tarhini) + - Remove uvloop for windows setup (Aleksandr Kurlov) + - Auto Reload (Yaser Amari) + - Documentation updates/fixups (multiple contributors) - - Fixes: - - Fix: auto_reload in Linux (Ashley Sommer) - - Fix: broken tests for aiohttp >= 3.3.0 (Ashley Sommer) - - Fix: disable auto_reload by default on windows (abuckenheimer) - - Fix (1143): Turn off access log with gunicorn (hqy) - - Fix (1268): Support status code for file response (Cosmo Borsky) - - Fix (1266): Add content_type flag to Sanic.static (Cosmo Borsky) - - Fix: subprotocols parameter missing from add_websocket_route (ciscorn) - - Fix (1242): Responses for CI header (yunstanford) - - Fix (1237): add version constraint for websockets (yunstanford) - - Fix (1231): memory leak - always release resource (Phillip Xu) - - Fix (1221): make request truthy if transport exists (Raphael Deems) - - Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer) - - Fix try_everything examples (PyManiacGR, kot83) - - Fix (1158): default to auto_reload in debug mode (Raphael Deems) - - Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux) - - Fix: raw requires bytes-like object (cloudship) - - Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe) - - Fix: Bug in multipart/form-data parser (DirkGuijt) - - Fix: Exception for missing parameter when value is null (NyanKiyoshi) - - Fix: Parameter check (Howie Hu) - - Fix (1089): Routing issue with named parameters and different methods (yunstanford) - - Fix (1085): Signal handling in multi-worker mode (yunstanford) - - Fix: single quote in readme.rst (Cosven) - - Fix: method typos (Dmitry Dygalo) - - Fix: log_response correct output for ip and port (Wibowo Arindrarto) - - Fix (1042): Exception Handling (Raphael Deems) - - Fix: Chinese URIs (Howie Hu) - - Fix (1079): timeout bug when self.transport is None (Raphael Deems) - - Fix (1074): fix strict_slashes when route has slash (Raphael Deems) - - Fix (1050): add samesite cookie to cookie keys (Raphael Deems) - - Fix (1065): allow add_task after server starts (Raphael Deems) - - Fix (1061): double quotes in unauthorized exception (Raphael Deems) - - Fix (1062): inject the app in add_task method (Raphael Deems) - - Fix: update environment.yml for readthedocs (Eli Uriegas) - - Fix: Cancel request task when response timeout is triggered (Jeong YunWon) - - Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deems) - - Fix: IPv6 Address and Socket Data Format (Dan Palmer) + - Fixes: + - Fix: auto_reload in Linux (Ashley Sommer) + - Fix: broken tests for aiohttp >= 3.3.0 (Ashley Sommer) + - Fix: disable auto_reload by default on windows (abuckenheimer) + - Fix (1143): Turn off access log with gunicorn (hqy) + - Fix (1268): Support status code for file response (Cosmo Borsky) + - Fix (1266): Add content_type flag to Sanic.static (Cosmo Borsky) + - Fix: subprotocols parameter missing from add_websocket_route (ciscorn) + - Fix (1242): Responses for CI header (yunstanford) + - Fix (1237): add version constraint for websockets (yunstanford) + - Fix (1231): memory leak - always release resource (Phillip Xu) + - Fix (1221): make request truthy if transport exists (Raphael Deems) + - Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer) + - Fix try_everything examples (PyManiacGR, kot83) + - Fix (1158): default to auto_reload in debug mode (Raphael Deems) + - Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux) + - Fix: raw requires bytes-like object (cloudship) + - Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe) + - Fix: Bug in multipart/form-data parser (DirkGuijt) + - Fix: Exception for missing parameter when value is null (NyanKiyoshi) + - Fix: Parameter check (Howie Hu) + - Fix (1089): Routing issue with named parameters and different methods (yunstanford) + - Fix (1085): Signal handling in multi-worker mode (yunstanford) + - Fix: single quote in readme.rst (Cosven) + - Fix: method typos (Dmitry Dygalo) + - Fix: log_response correct output for ip and port (Wibowo Arindrarto) + - Fix (1042): Exception Handling (Raphael Deems) + - Fix: Chinese URIs (Howie Hu) + - Fix (1079): timeout bug when self.transport is None (Raphael Deems) + - Fix (1074): fix strict_slashes when route has slash (Raphael Deems) + - Fix (1050): add samesite cookie to cookie keys (Raphael Deems) + - Fix (1065): allow add_task after server starts (Raphael Deems) + - Fix (1061): double quotes in unauthorized exception (Raphael Deems) + - Fix (1062): inject the app in add_task method (Raphael Deems) + - Fix: update environment.yml for readthedocs (Eli Uriegas) + - Fix: Cancel request task when response timeout is triggered (Jeong YunWon) + - Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deems) + - Fix: IPv6 Address and Socket Data Format (Dan Palmer) Note: Changelog was unmaintained between 0.1 and 0.7 From 96912f436dae0cdb41dbf91ea3a69cc229113c3a Mon Sep 17 00:00:00 2001 From: Stephen Sadowski Date: Mon, 24 Sep 2018 09:05:58 -0500 Subject: [PATCH 27/32] Corrected Raphael Deem's name in changelog - sorry @r0fls! --- CHANGELOG.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbff316c..a5d3b7f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ Version 0.8 - Add weakref to request slots (vopankov) - remove ubuntu 12.04 from test fixture due to deprecation (yunstanford) - Allow streaming handlers in add_route (kinware) - - use travis_retry for tox (Raphael Deems) + - use travis_retry for tox (Raphael Deem) - update aiohttp version for test client (yunstanford) - add redirect import for clarity (yingshaoxo) - Update HTTP Entity headers (Arnulfo Solís) @@ -45,10 +45,10 @@ Version 0.8 - Fix (1242): Responses for CI header (yunstanford) - Fix (1237): add version constraint for websockets (yunstanford) - Fix (1231): memory leak - always release resource (Phillip Xu) - - Fix (1221): make request truthy if transport exists (Raphael Deems) + - Fix (1221): make request truthy if transport exists (Raphael Deem) - Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer) - Fix try_everything examples (PyManiacGR, kot83) - - Fix (1158): default to auto_reload in debug mode (Raphael Deems) + - Fix (1158): default to auto_reload in debug mode (Raphael Deem) - Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux) - Fix: raw requires bytes-like object (cloudship) - Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe) @@ -60,17 +60,17 @@ Version 0.8 - Fix: single quote in readme.rst (Cosven) - Fix: method typos (Dmitry Dygalo) - Fix: log_response correct output for ip and port (Wibowo Arindrarto) - - Fix (1042): Exception Handling (Raphael Deems) + - Fix (1042): Exception Handling (Raphael Deem) - Fix: Chinese URIs (Howie Hu) - - Fix (1079): timeout bug when self.transport is None (Raphael Deems) - - Fix (1074): fix strict_slashes when route has slash (Raphael Deems) - - Fix (1050): add samesite cookie to cookie keys (Raphael Deems) - - Fix (1065): allow add_task after server starts (Raphael Deems) - - Fix (1061): double quotes in unauthorized exception (Raphael Deems) - - Fix (1062): inject the app in add_task method (Raphael Deems) + - Fix (1079): timeout bug when self.transport is None (Raphael Deem) + - Fix (1074): fix strict_slashes when route has slash (Raphael Deem) + - Fix (1050): add samesite cookie to cookie keys (Raphael Deem) + - Fix (1065): allow add_task after server starts (Raphael Deem) + - Fix (1061): double quotes in unauthorized exception (Raphael Deem) + - Fix (1062): inject the app in add_task method (Raphael Deem) - Fix: update environment.yml for readthedocs (Eli Uriegas) - Fix: Cancel request task when response timeout is triggered (Jeong YunWon) - - Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deems) + - Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deem) - Fix: IPv6 Address and Socket Data Format (Dan Palmer) Note: Changelog was unmaintained between 0.1 and 0.7 From bcc11fa7fe598c9d41476513968a5db4e7e4a7dd Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Sun, 30 Sep 2018 09:36:55 +0100 Subject: [PATCH 28/32] Fix whitespace in error message --- sanic/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/app.py b/sanic/app.py index acdf70c8..5af21751 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -170,7 +170,7 @@ class Sanic: return handler else: raise ValueError( - 'Required parameter `request` missing' + 'Required parameter `request` missing ' 'in the {0}() route?'.format( handler.__name__)) From d100f54551d6b6c888738508c386091fa3927d50 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Sun, 30 Sep 2018 15:59:16 +0200 Subject: [PATCH 29/32] Check error message and fix some lint error in test config. --- tests/test_config.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index 3db35f4d..14b28d46 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -15,24 +15,28 @@ def test_load_from_object(app): assert app.config.CONFIG_VALUE == 'should be used' assert 'not_for_config' not in app.config + def test_auto_load_env(): environ["SANIC_TEST_ANSWER"] = "42" app = Sanic() assert app.config.TEST_ANSWER == 42 del environ["SANIC_TEST_ANSWER"] + def test_dont_load_env(): environ["SANIC_TEST_ANSWER"] = "42" app = Sanic(load_env=False) - assert getattr(app.config, 'TEST_ANSWER', None) == None + assert getattr(app.config, 'TEST_ANSWER', None) is None del environ["SANIC_TEST_ANSWER"] + def test_load_env_prefix(): environ["MYAPP_TEST_ANSWER"] = "42" app = Sanic(load_env='MYAPP_') assert app.config.TEST_ANSWER == 42 del environ["MYAPP_TEST_ANSWER"] + def test_load_from_file(app): config = b""" VALUE = 'some value' @@ -68,12 +72,16 @@ def test_load_from_envvar(app): def test_load_from_missing_envvar(app): - with pytest.raises(RuntimeError): + with pytest.raises(RuntimeError) as e: app.config.from_envvar('non-existent variable') + assert str(e.value) == ("The environment variable 'non-existent " + "variable' is not set and thus configuration " + "could not be loaded.") def test_overwrite_exisiting_config(app): app.config.DEFAULT = 1 + class Config: DEFAULT = 2 @@ -82,5 +90,6 @@ def test_overwrite_exisiting_config(app): def test_missing_config(app): - with pytest.raises(AttributeError): + with pytest.raises(AttributeError) as e: app.config.NON_EXISTENT + assert str(e.value) == ("Config has no 'NON_EXISTENT'") From 790047e450a696f87b71d8246e133af7f80828c3 Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Wed, 3 Oct 2018 10:59:24 +1000 Subject: [PATCH 30/32] Fixes #1340 --- sanic/server.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index d5a8f211..17529a32 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -150,10 +150,7 @@ class HttpProtocol(asyncio.Protocol): self._request_stream_task.cancel() if self._request_handler_task: self._request_handler_task.cancel() - try: - raise RequestTimeout('Request Timeout') - except RequestTimeout as exception: - self.write_error(exception) + self.write_error(RequestTimeout('Request Timeout')) def response_timeout_callback(self): # Check if elapsed time since response was initiated exceeds our @@ -170,10 +167,7 @@ class HttpProtocol(asyncio.Protocol): self._request_stream_task.cancel() if self._request_handler_task: self._request_handler_task.cancel() - try: - raise ServiceUnavailable('Response Timeout') - except ServiceUnavailable as exception: - self.write_error(exception) + self.write_error(ServiceUnavailable('Response Timeout')) def keep_alive_timeout_callback(self): # Check if elapsed time since last response exceeds our configured @@ -199,8 +193,7 @@ class HttpProtocol(asyncio.Protocol): # memory limits self._total_request_size += len(data) if self._total_request_size > self.request_max_size: - exception = PayloadTooLarge('Payload Too Large') - self.write_error(exception) + self.write_error(PayloadTooLarge('Payload Too Large')) # Create parser if this is the first time we're receiving data if self.parser is None: @@ -218,8 +211,7 @@ class HttpProtocol(asyncio.Protocol): message = 'Bad Request' if self._debug: message += '\n' + traceback.format_exc() - exception = InvalidUsage(message) - self.write_error(exception) + self.write_error(InvalidUsage(message)) def on_url(self, url): if not self.url: @@ -233,8 +225,7 @@ class HttpProtocol(asyncio.Protocol): if value is not None: if self._header_fragment == b'Content-Length' \ and int(value) > self.request_max_size: - exception = PayloadTooLarge('Payload Too Large') - self.write_error(exception) + self.write_error(PayloadTooLarge('Payload Too Large')) try: value = value.decode() except UnicodeDecodeError: @@ -433,7 +424,7 @@ class HttpProtocol(asyncio.Protocol): self.log_response(response) try: self.transport.close() - except AttributeError as e: + except AttributeError: logger.debug('Connection lost before server could close it.') def bail_out(self, message, from_error=False): @@ -443,8 +434,7 @@ class HttpProtocol(asyncio.Protocol): self.transport.get_extra_info('peername')) logger.debug('Exception:\n%s', traceback.format_exc()) else: - exception = ServerError(message) - self.write_error(exception) + self.write_error(ServerError(message)) logger.error(message) def cleanup(self): From d1a578b555b250652ff61231f5ac1e65fe9cc711 Mon Sep 17 00:00:00 2001 From: Richard Kuesters Date: Wed, 3 Oct 2018 12:22:29 -0300 Subject: [PATCH 31/32] pinned httptools requirement to version 0.0.10+ --- environment.yml | 2 +- requirements-dev.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/environment.yml b/environment.yml index 9f416c0e..e13c76fe 100644 --- a/environment.yml +++ b/environment.yml @@ -12,7 +12,7 @@ dependencies: - zlib=1.2.8=0 - pip: - uvloop>=0.5.3 - - httptools>=0.0.9 + - httptools>=0.0.10 - ujson>=1.35 - aiofiles>=0.3.0 - websockets>=3.2 diff --git a/requirements-dev.txt b/requirements-dev.txt index 004f6f9e..12b29a2b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ aiohttp>=2.3.0,<=3.2.1 chardet<=2.3.0 beautifulsoup4 coverage -httptools +httptools>=0.0.10 flake8 pytest==3.3.2 tox diff --git a/requirements.txt b/requirements.txt index e320e781..74d9bf83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ aiofiles -httptools +httptools>=0.0.10 ujson; sys_platform != "win32" and implementation_name == "cpython" uvloop; sys_platform != "win32" and implementation_name == "cpython" websockets>=5.0,<6.0 diff --git a/setup.py b/setup.py index 34703ab4..2ce1510f 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ ujson = 'ujson>=1.35' + env_dependency uvloop = 'uvloop>=0.5.3' + env_dependency requirements = [ - 'httptools>=0.0.9', + 'httptools>=0.0.10', uvloop, ujson, 'aiofiles>=0.3.0', From b689037984136aaa325a1c5d9c29b291bb77fd8c Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Thu, 4 Oct 2018 12:31:57 +0300 Subject: [PATCH 32/32] Update README.rst --- README.rst | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 19b0a9f8..2b3f1606 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's ba On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy. -Sanic is developed `on GitHub `_. Contributions are welcome! +Sanic is developed `on GitHub `_. We also have `a community discussion board `_. Contributions are welcome! If you have a project that utilizes Sanic make sure to comment on the `issue `_ that we use to track those projects! @@ -56,6 +56,12 @@ Documentation .. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg :target: https://pypi.python.org/pypi/sanic/ + +Questions and Discussion +------------------------ + +`Ask a question or join the conversation `_. + Examples -------- @@ -66,14 +72,6 @@ Examples `Projects `_. Sanic in production use. -TODO ----- - * http2 - -Limitations ------------ -* No wheels for uvloop and httptools on Windows :( - Final Thoughts --------------