diff --git a/docs/sanic/middleware.md b/docs/sanic/middleware.md index 1a7f9d86..228c9d47 100644 --- a/docs/sanic/middleware.md +++ b/docs/sanic/middleware.md @@ -110,3 +110,23 @@ async def notify_server_started_after_five_seconds(): app.add_task(notify_server_started_after_five_seconds()) ``` + +Sanic will attempt to automatically inject the app, passing it as an argument to the task: + +```python +async def notify_server_started_after_five_seconds(app): + await asyncio.sleep(5) + print(app.name) + +app.add_task(notify_server_started_after_five_seconds) +``` + +Or you can pass the app explicitly for the same effect: + +```python +async def notify_server_started_after_five_seconds(app): + await asyncio.sleep(5) + print(app.name) + +app.add_task(notify_server_started_after_five_seconds(app)) +` diff --git a/environment.yml b/environment.yml index 298ea552..1c1dd82f 100644 --- a/environment.yml +++ b/environment.yml @@ -16,4 +16,5 @@ dependencies: - ujson>=1.35 - aiofiles>=0.3.0 - websockets>=3.2 - - https://github.com/channelcat/docutils-fork/zipball/master \ No newline at end of file + - sphinxcontrib-asyncio>=0.2.0 + - https://github.com/channelcat/docutils-fork/zipball/master diff --git a/sanic/app.py b/sanic/app.py index 23d404f1..26ef83b0 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -86,12 +86,24 @@ class Sanic: :param task: future, couroutine or awaitable """ - @self.listener('before_server_start') - def run(app, loop): + try: if callable(task): - loop.create_task(task()) + try: + self.loop.create_task(task(self)) + except TypeError: + self.loop.create_task(task()) else: - loop.create_task(task) + self.loop.create_task(task) + except SanicException: + @self.listener('before_server_start') + def run(app, loop): + if callable(task): + try: + loop.create_task(task(self)) + except TypeError: + loop.create_task(task()) + else: + loop.create_task(task) # Decorator def listener(self, event): diff --git a/sanic/cookies.py b/sanic/cookies.py index 8ad8cbfc..f4cbf6a3 100644 --- a/sanic/cookies.py +++ b/sanic/cookies.py @@ -83,6 +83,7 @@ class Cookie(dict): "secure": "Secure", "httponly": "HttpOnly", "version": "Version", + "samesite": "SameSite", } _flags = {'secure', 'httponly'} diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 0133fd64..6da747f2 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -263,7 +263,7 @@ class Unauthorized(SanicException): # if auth-scheme is specified, set "WWW-Authenticate" header if scheme is not None: - values = ["{!s}={!r}".format(k, v) for k, v in kwargs.items()] + values = ['{!s}="{!s}"'.format(k, v) for k, v in kwargs.items()] challenge = ', '.join(values) self.headers = { diff --git a/sanic/router.py b/sanic/router.py index 2383b915..0d8305ef 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -129,18 +129,22 @@ class Router: # Add versions with and without trailing / slashed_methods = self.routes_all.get(uri + '/', frozenset({})) + unslashed_methods = self.routes_all.get(uri[:-1], frozenset({})) if isinstance(methods, Iterable): _slash_is_missing = all(method in slashed_methods for method in methods) + _without_slash_is_missing = all(method in unslashed_methods for + method in methods) else: _slash_is_missing = methods in slashed_methods + _without_slash_is_missing = methods in unslashed_methods slash_is_missing = ( not uri[-1] == '/' and not _slash_is_missing ) without_slash_is_missing = ( uri[-1] == '/' and not - self.routes_all.get(uri[:-1], False) and not + _without_slash_is_missing and not uri == '/' ) # add version with trailing slash diff --git a/sanic/server.py b/sanic/server.py index d608f8d7..af6dc62d 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -431,7 +431,10 @@ class HttpProtocol(asyncio.Protocol): if self.parser and (self.keep_alive or getattr(response, 'status', 0) == 408): self.log_response(response) - self.transport.close() + try: + self.transport.close() + except AttributeError as e: + logger.debug('Connection lost before server could close it.') def bail_out(self, message, from_error=False): if from_error or self.transport.is_closing(): diff --git a/setup.py b/setup.py index 3f8cd7ce..a258d58a 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,8 @@ if strtobool(os.environ.get("SANIC_NO_UJSON", "no")): print("Installing without uJSON") requirements.remove(ujson) -if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")): +# 'nt' means windows OS +if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")) or os.name == 'nt': print("Installing without uvLoop") requirements.remove(uvloop) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..dd3dc6d7 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,23 @@ +import re +import sanic + + +def pytest_collection_modifyitems(session, config, items): + base_port = sanic.testing.PORT + + worker_id = getattr(config, 'slaveinput', {}).get('slaveid', 'master') + m = re.search(r'[0-9]+', worker_id) + if m: + num_id = int(m.group(0)) + 1 + else: + num_id = 0 + new_port = base_port + num_id + + def new_test_client(app, port=new_port): + return sanic.testing.SanicTestClient(app, port) + + sanic.Sanic.test_port = new_port + sanic.Sanic.test_client = property(new_test_client) + + app = sanic.Sanic() + assert app.test_client.port == new_port diff --git a/tests/test_create_task.py b/tests/test_create_task.py index d7418466..1517ca8c 100644 --- a/tests/test_create_task.py +++ b/tests/test_create_task.py @@ -2,6 +2,7 @@ from sanic import Sanic from sanic.response import text from threading import Event import asyncio +from queue import Queue def test_create_task(): @@ -28,3 +29,19 @@ 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') + q = Queue() + + @app.route('/') + def not_set(request): + return "hello" + + async def coro(app): + q.put(app.name) + + app.add_task(coro) + + request, response = app.test_client.get('/') + assert q.get() == 'test_add_task' diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 16d08459..b4e2c6ea 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -138,7 +138,7 @@ def test_unauthorized_exception(exception_app): request, response = exception_app.test_client.get('/401/basic') assert response.status == 401 assert response.headers.get('WWW-Authenticate') is not None - assert response.headers.get('WWW-Authenticate') == "Basic realm='Sanic'" + assert response.headers.get('WWW-Authenticate') == 'Basic realm="Sanic"' request, response = exception_app.test_client.get('/401/digest') assert response.status == 401 @@ -146,10 +146,10 @@ def test_unauthorized_exception(exception_app): auth_header = response.headers.get('WWW-Authenticate') assert auth_header is not None assert auth_header.startswith('Digest') - assert "qop='auth, auth-int'" in auth_header - assert "algorithm='MD5'" in auth_header - assert "nonce='abcdef'" in auth_header - assert "opaque='zyxwvu'" in auth_header + assert 'qop="auth, auth-int"' in auth_header + assert 'algorithm="MD5"' in auth_header + assert 'nonce="abcdef"' in auth_header + assert 'opaque="zyxwvu"' in auth_header request, response = exception_app.test_client.get('/401/bearer') assert response.status == 401 diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index 15f6d705..c3de8462 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -7,7 +7,7 @@ from sanic.config import Config from sanic import server import aiohttp from aiohttp import TCPConnector -from sanic.testing import SanicTestClient, HOST, PORT +from sanic.testing import SanicTestClient, HOST class ReuseableTCPConnector(TCPConnector): @@ -30,7 +30,7 @@ class ReuseableTCPConnector(TCPConnector): class ReuseableSanicTestClient(SanicTestClient): def __init__(self, app, loop=None): - super(ReuseableSanicTestClient, self).__init__(app) + super().__init__(app, port=app.test_port) if loop is None: loop = asyncio.get_event_loop() self._loop = loop @@ -68,13 +68,14 @@ class ReuseableSanicTestClient(SanicTestClient): import traceback traceback.print_tb(e2.__traceback__) exceptions.append(e2) - #Don't stop here! self.app.stop() + # Don't stop here! self.app.stop() if self._server is not None: _server = self._server else: _server_co = self.app.create_server(host=HOST, debug=debug, - port=PORT, **server_kwargs) + port=self.app.test_port, + **server_kwargs) server.trigger_events( self.app.listeners['before_server_start'], loop) @@ -88,7 +89,7 @@ class ReuseableSanicTestClient(SanicTestClient): raise e1 self._server = _server = http_server server.trigger_events( - self.app.listeners['after_server_start'], loop) + self.app.listeners['after_server_start'], loop) self.app.listeners['after_server_start'].pop() if do_kill_server: @@ -133,7 +134,7 @@ class ReuseableSanicTestClient(SanicTestClient): url = uri else: url = 'http://{host}:{port}{uri}'.format( - host=HOST, port=PORT, uri=uri) + host=HOST, port=self.port, uri=uri) do_kill_session = kwargs.pop('end_session', False) if self._session: session = self._session diff --git a/tests/test_logging.py b/tests/test_logging.py index e95b7ce5..1a040a5c 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -101,7 +101,6 @@ def test_log_connection_lost(debug, monkeypatch): log = stream.getvalue() if debug: - assert log.startswith( - 'Connection lost before response written @') + assert 'Connection lost before response written @' in log else: assert 'Connection lost before response written @' not in log diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index 1342c6d3..519ad171 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -3,7 +3,7 @@ import random import signal from sanic import Sanic -from sanic.testing import HOST, PORT +from sanic.testing import HOST def test_multiprocessing(): @@ -20,7 +20,7 @@ def test_multiprocessing(): signal.signal(signal.SIGALRM, stop_on_alarm) signal.alarm(1) - app.run(HOST, PORT, workers=num_workers) + app.run(HOST, app.test_port, workers=num_workers) assert len(process_list) == num_workers diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index a1d8a885..113ffdd7 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -6,7 +6,7 @@ from sanic.response import text from sanic.config import Config import aiohttp from aiohttp import TCPConnector -from sanic.testing import SanicTestClient, HOST, PORT +from sanic.testing import SanicTestClient, HOST class DelayableTCPConnector(TCPConnector): @@ -96,7 +96,7 @@ class DelayableTCPConnector(TCPConnector): class DelayableSanicTestClient(SanicTestClient): def __init__(self, app, loop, request_delay=1): - super(DelayableSanicTestClient, self).__init__(app) + super(DelayableSanicTestClient, self).__init__(app, port=app.test_port) self._request_delay = request_delay self._loop = None @@ -108,7 +108,7 @@ class DelayableSanicTestClient(SanicTestClient): url = uri else: url = 'http://{host}:{port}{uri}'.format( - host=HOST, port=PORT, uri=uri) + host=HOST, port=self.port, uri=uri) conn = DelayableTCPConnector(pre_request_delay=self._request_delay, verify_ssl=False, loop=self._loop) async with aiohttp.ClientSession(cookies=cookies, connector=conn, diff --git a/tests/test_requests.py b/tests/test_requests.py index e47520c4..9eb88243 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -9,7 +9,7 @@ from sanic import Sanic from sanic.exceptions import ServerError from sanic.response import json, text from sanic.request import DEFAULT_HTTP_CONTENT_TYPE -from sanic.testing import HOST, PORT +from sanic.testing import HOST # ------------------------------------------------------------ # @@ -338,7 +338,7 @@ def test_url_attributes_no_ssl(path, query, expected_url): app.add_route(handler, path) request, response = app.test_client.get(path + '?{}'.format(query)) - assert request.url == expected_url.format(HOST, PORT) + assert request.url == expected_url.format(HOST, app.test_port) parsed = urlparse(request.url) @@ -369,9 +369,9 @@ def test_url_attributes_with_ssl(path, query, expected_url): app.add_route(handler, path) request, response = app.test_client.get( - 'https://{}:{}'.format(HOST, PORT) + path + '?{}'.format(query), + 'https://{}:{}'.format(HOST, app.test_port) + path + '?{}'.format(query), server_kwargs={'ssl': context}) - assert request.url == expected_url.format(HOST, PORT) + assert request.url == expected_url.format(HOST, app.test_port) parsed = urlparse(request.url) diff --git a/tests/test_response.py b/tests/test_response.py index 086b4e58..6ac77cec 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -10,7 +10,7 @@ from random import choice from sanic import Sanic from sanic.response import HTTPResponse, stream, StreamingHTTPResponse, file, file_stream, json -from sanic.testing import HOST, PORT +from sanic.testing import HOST from unittest.mock import MagicMock JSON_DATA = {'ok': True} @@ -139,7 +139,7 @@ def test_stream_response_writes_correct_content_to_transport(streaming_app): app.stop() - streaming_app.run(host=HOST, port=PORT) + streaming_app.run(host=HOST, port=streaming_app.test_port) @pytest.fixture diff --git a/tests/test_routes.py b/tests/test_routes.py index b6b62283..fb47203f 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -129,6 +129,30 @@ def test_route_strict_slash_default_value_can_be_overwritten(): request, response = app.test_client.get('/get/') assert response.text == 'OK' +def test_route_slashes_overload(): + app = Sanic('test_route_slashes_overload') + + @app.get('/hello/') + def handler(request): + return text('OK') + + @app.post('/hello/') + def handler(request): + return text('OK') + + + request, response = app.test_client.get('/hello') + assert response.text == 'OK' + + request, response = app.test_client.get('/hello/') + assert response.text == 'OK' + + request, response = app.test_client.post('/hello') + assert response.text == 'OK' + + request, response = app.test_client.post('/hello/') + assert response.text == 'OK' + def test_route_optional_slash(): app = Sanic('test_route_optional_slash') diff --git a/tests/test_server_events.py b/tests/test_server_events.py index d78f0aed..ab0a1fb1 100644 --- a/tests/test_server_events.py +++ b/tests/test_server_events.py @@ -6,7 +6,7 @@ import signal import pytest from sanic import Sanic -from sanic.testing import HOST, PORT +from sanic.testing import HOST AVAILABLE_LISTENERS = [ 'before_server_start', @@ -31,7 +31,7 @@ def start_stop_app(random_name_app, **run_kwargs): signal.signal(signal.SIGALRM, stop_on_alarm) signal.alarm(1) try: - random_name_app.run(HOST, PORT, **run_kwargs) + random_name_app.run(HOST, random_name_app.test_port, **run_kwargs) except KeyboardInterrupt: pass diff --git a/tests/test_signal_handlers.py b/tests/test_signal_handlers.py index c4cca92b..967e903f 100644 --- a/tests/test_signal_handlers.py +++ b/tests/test_signal_handlers.py @@ -1,8 +1,7 @@ from sanic import Sanic from sanic.response import HTTPResponse -from sanic.testing import HOST, PORT +from sanic.testing import HOST from unittest.mock import MagicMock -import pytest import asyncio from queue import Queue @@ -31,7 +30,7 @@ def test_register_system_signals(): app.listener('before_server_start')(set_loop) app.listener('after_server_stop')(after) - app.run(HOST, PORT) + app.run(HOST, app.test_port) assert calledq.get() == True @@ -47,5 +46,5 @@ def test_dont_register_system_signals(): app.listener('before_server_start')(set_loop) app.listener('after_server_stop')(after) - app.run(HOST, PORT, register_sys_signals=False) + app.run(HOST, app.test_port, register_sys_signals=False) assert calledq.get() == False diff --git a/tests/test_url_building.py b/tests/test_url_building.py index f3ed534f..ed41b017 100644 --- a/tests/test_url_building.py +++ b/tests/test_url_building.py @@ -5,7 +5,7 @@ from sanic import Sanic from sanic.response import text from sanic.views import HTTPMethodView from sanic.blueprints import Blueprint -from sanic.testing import PORT as test_port, HOST as test_host +from sanic.testing import HOST as test_host from sanic.exceptions import URLBuildError import string @@ -15,11 +15,11 @@ URL_FOR_VALUE1 = '/myurl?arg1=v1&arg1=v2' URL_FOR_ARGS2 = dict(arg1=['v1', 'v2'], _anchor='anchor') URL_FOR_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor' URL_FOR_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http', - _server='{}:{}'.format(test_host, test_port), _external=True) -URL_FOR_VALUE3 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port) + _server='{}:PORT_PLACEHOLDER'.format(test_host), _external=True) +URL_FOR_VALUE3 = 'http://{}:PORT_PLACEHOLDER/myurl?arg1=v1#anchor'.format(test_host) URL_FOR_ARGS4 = dict(arg1='v1', _anchor='anchor', _external=True, - _server='http://{}:{}'.format(test_host, test_port),) -URL_FOR_VALUE4 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port) + _server='http://{}:PORT_PLACEHOLDER'.format(test_host),) +URL_FOR_VALUE4 = 'http://{}:PORT_PLACEHOLDER/myurl?arg1=v1#anchor'.format(test_host) def _generate_handlers_from_names(app, l): @@ -61,6 +61,10 @@ def test_simple_url_for_getting_with_more_params(args, url): def passes(request): return text('this should pass') + if '_server' in args: + args['_server'] = args['_server'].replace( + 'PORT_PLACEHOLDER', str(app.test_port)) + url = url.replace('PORT_PLACEHOLDER', str(app.test_port)) assert url == app.url_for('passes', **args) request, response = app.test_client.get(url) assert response.status == 200 diff --git a/tox.ini b/tox.ini index ff43a139..0b573f6b 100644 --- a/tox.ini +++ b/tox.ini @@ -12,12 +12,13 @@ deps = pytest-cov pytest-sanic pytest-sugar + pytest-xdist aiohttp==1.3.5 chardet<=2.3.0 beautifulsoup4 gunicorn commands = - pytest tests --cov sanic --cov-report= {posargs} + pytest tests -n 4 --cov sanic --cov-report= {posargs} - coverage combine --append coverage report -m