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/docs/sanic/testing.md b/docs/sanic/testing.md index 0aca9184..00a604bf 100644 --- a/docs/sanic/testing.md +++ b/docs/sanic/testing.md @@ -20,7 +20,7 @@ def test_index_put_not_allowed(): assert response.status == 405 ``` -Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.01:42101` and +Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and your test request is executed against your application, using `aiohttp`. The `test_client` methods accept the following arguments and keyword arguments: 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/__init__.py b/sanic/__init__.py index 8f35a283..78bc7bd9 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.6.0' +__version__ = '0.7.0' __all__ = ['Sanic', 'Blueprint'] diff --git a/sanic/app.py b/sanic/app.py index 05d99c08..cf8f63d8 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): @@ -544,6 +556,7 @@ class Sanic: # Fetch handler from router handler, args, kwargs, uri = self.router.get(request) + request.uri_template = uri if handler is None: raise ServerError( @@ -564,13 +577,17 @@ class Sanic: if isawaitable(response): response = await response except Exception as e: - if self.debug: + if isinstance(e, SanicException): + response = self.error_handler.default(request=request, + exception=e) + elif self.debug: response = HTTPResponse( "Error while handling error: {}\nStack: {}".format( - e, format_exc())) + e, format_exc()), status=500) else: response = HTTPResponse( - "An error occurred while handling an error") + "An error occurred while handling an error", + status=500) finally: # -------------------------------------------- # # Response Middleware 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 aa1e0d4d..6da747f2 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -150,6 +150,16 @@ class InvalidUsage(SanicException): pass +@add_status_code(405) +class MethodNotSupported(SanicException): + def __init__(self, message, method, allowed_methods): + super().__init__(message) + self.headers = dict() + self.headers["Allow"] = ", ".join(allowed_methods) + if method in ['HEAD', 'PATCH', 'PUT', 'DELETE']: + self.headers['Content-Length'] = 0 + + @add_status_code(500) class ServerError(SanicException): pass @@ -167,8 +177,6 @@ class URLBuildError(ServerError): class FileNotFound(NotFound): - pass - def __init__(self, message, path, relative_url): super().__init__(message) self.path = path @@ -198,8 +206,6 @@ class HeaderNotFound(InvalidUsage): @add_status_code(416) class ContentRangeError(SanicException): - pass - def __init__(self, message, content_range): super().__init__(message) self.headers = { @@ -257,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/request.py b/sanic/request.py index d4f6dc6f..ecc41d13 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -1,5 +1,6 @@ import sys import json +import socket from cgi import parse_header from collections import namedtuple from http.cookies import SimpleCookie @@ -181,13 +182,22 @@ class Request(dict): @property def socket(self): if not hasattr(self, '_socket'): - self._get_socket() + self._get_address() return self._socket def _get_address(self): - self._socket = (self.transport.get_extra_info('peername') or - (None, None)) - self._ip, self._port = self._socket + sock = self.transport.get_extra_info('socket') + + if sock.family == socket.AF_INET: + self._socket = (self.transport.get_extra_info('peername') or + (None, None)) + self._ip, self._port = self._socket + elif sock.family == socket.AF_INET6: + self._socket = (self.transport.get_extra_info('peername') or + (None, None, None, None)) + self._ip, self._port, *_ = self._socket + else: + self._ip, self._port = (None, None) @property def remote_addr(self): diff --git a/sanic/router.py b/sanic/router.py index 208e3772..bd74c11d 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -2,8 +2,9 @@ import re from collections import defaultdict, namedtuple from collections.abc import Iterable from functools import lru_cache +from urllib.parse import unquote -from sanic.exceptions import NotFound, InvalidUsage +from sanic.exceptions import NotFound, MethodNotSupported from sanic.views import CompositionView Route = namedtuple( @@ -129,18 +130,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 @@ -350,6 +355,16 @@ class Router: except NotFound: return self._get(request.path, request.method, '') + def get_supported_methods(self, url): + """Get a list of supported methods for a url and optional host. + + :param url: URL string (including host) + :return: frozenset of supported methods + """ + route = self.routes_all.get(url) + # if methods are None then this logic will prevent an error + return getattr(route, 'methods', None) or frozenset() + @lru_cache(maxsize=ROUTER_CACHE_SIZE) def _get(self, url, method, host): """Get a request handler based on the URL of the request, or raises an @@ -359,12 +374,13 @@ class Router: :param method: request method :return: handler, arguments, keyword arguments """ - url = host + url + url = unquote(host + url) # Check against known static routes route = self.routes_static.get(url) - method_not_supported = InvalidUsage( - 'Method {} not allowed for URL {}'.format( - method, url), status_code=405) + method_not_supported = MethodNotSupported( + 'Method {} not allowed for URL {}'.format(method, url), + method=method, + allowed_methods=self.get_supported_methods(url)) if route: if route.methods and method not in route.methods: raise method_not_supported @@ -407,7 +423,7 @@ class Router: """ try: handler = self.get(request)[0] - except (NotFound, InvalidUsage): + except (NotFound, MethodNotSupported): return False if (hasattr(handler, 'view_class') and hasattr(handler.view_class, request.method.lower())): diff --git a/sanic/server.py b/sanic/server.py index a475fd98..e9790dae 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -174,6 +174,10 @@ class HttpProtocol(asyncio.Protocol): self.response_timeout_callback) ) else: + if self._request_stream_task: + self._request_stream_task.cancel() + if self._request_handler_task: + self._request_handler_task.cancel() try: raise ServiceUnavailable('Response Timeout') except ServiceUnavailable as exception: @@ -312,13 +316,15 @@ class HttpProtocol(asyncio.Protocol): else: extra['byte'] = -1 + extra['host'] = 'UNKNOWN' if self.request is not None: - extra['host'] = '{0}:{1}'.format(self.request.ip[0], - self.request.ip[1]) + if self.request.ip: + extra['host'] = '{0}:{1}'.format(self.request.ip, + self.request.port) + extra['request'] = '{0} {1}'.format(self.request.method, self.request.url) else: - extra['host'] = 'UNKNOWN' extra['request'] = 'nil' access_logger.info('', extra=extra) @@ -426,7 +432,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(): @@ -635,7 +644,9 @@ def serve(host, port, request_handler, error_handler, before_start=None, coros = [] for conn in connections: if hasattr(conn, "websocket") and conn.websocket: - coros.append(conn.websocket.close_connection(force=True)) + coros.append( + conn.websocket.close_connection(after_handshake=True) + ) else: conn.close() diff --git a/sanic/worker.py b/sanic/worker.py index 79c0a17d..3d8a4617 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -115,7 +115,9 @@ class GunicornWorker(base.Worker): coros = [] for conn in self.connections: if hasattr(conn, "websocket") and conn.websocket: - coros.append(conn.websocket.close_connection(force=True)) + coros.append( + conn.websocket.close_connection(after_handshake=False) + ) else: conn.close() _shutdown = asyncio.gather(*coros, loop=self.loop) diff --git a/setup.py b/setup.py index 17f2c67e..a258d58a 100644 --- a/setup.py +++ b/setup.py @@ -59,13 +59,14 @@ requirements = [ uvloop, ujson, 'aiofiles>=0.3.0', - 'websockets>=3.2', + 'websockets>=4.0', ] 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 910c4e80..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} @@ -35,6 +35,25 @@ async def sample_streaming_fn(response): await asyncio.sleep(.001) response.write('bar') +def test_method_not_allowed(): + app = Sanic('method_not_allowed') + + @app.get('/') + async def test(request): + return response.json({'hello': 'world'}) + + request, response = app.test_client.head('/') + assert response.headers['Allow']== 'GET' + + @app.post('/') + async def test(request): + return response.json({'hello': 'world'}) + + request, response = app.test_client.head('/') + assert response.status == 405 + assert set(response.headers['Allow'].split(', ')) == set(['GET', 'POST']) + assert response.headers['Content-Length'] == '0' + @pytest.fixture def json_app(): @@ -120,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 @@ -254,4 +273,4 @@ def test_file_stream_head_response(file_name, static_file_directory): assert 'Content-Length' in response.headers assert int(response.headers[ 'Content-Length']) == len( - get_file_content(static_file_directory, file_name)) \ No newline at end of file + get_file_content(static_file_directory, file_name)) diff --git a/tests/test_routes.py b/tests/test_routes.py index b6b62283..84d6b221 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -29,7 +29,8 @@ def test_versioned_routes_get(method): client_method = getattr(app.test_client, method) request, response = client_method('/v1/{}'.format(method)) - assert response.status== 200 + assert response.status == 200 + def test_shorthand_routes_get(): app = Sanic('test_shorhand_routes_get') @@ -44,6 +45,7 @@ def test_shorthand_routes_get(): request, response = app.test_client.post('/get') assert response.status == 405 + def test_shorthand_routes_multiple(): app = Sanic('test_shorthand_routes_multiple') @@ -62,6 +64,7 @@ def test_shorthand_routes_multiple(): request, response = app.test_client.options('/get/') assert response.status == 200 + def test_route_strict_slash(): app = Sanic('test_route_strict_slash') @@ -89,6 +92,7 @@ def test_route_strict_slash(): request, response = app.test_client.post('/post') assert response.status == 404 + def test_route_invalid_parameter_syntax(): with pytest.raises(ValueError): app = Sanic('test_route_invalid_param_syntax') @@ -99,6 +103,7 @@ def test_route_invalid_parameter_syntax(): request, response = app.test_client.get('/get') + def test_route_strict_slash_default_value(): app = Sanic('test_route_strict_slash', strict_slashes=True) @@ -109,6 +114,7 @@ def test_route_strict_slash_default_value(): request, response = app.test_client.get('/get/') assert response.status == 404 + def test_route_strict_slash_without_passing_default_value(): app = Sanic('test_route_strict_slash') @@ -119,6 +125,7 @@ def test_route_strict_slash_without_passing_default_value(): request, response = app.test_client.get('/get/') assert response.text == 'OK' + def test_route_strict_slash_default_value_can_be_overwritten(): app = Sanic('test_route_strict_slash', strict_slashes=True) @@ -129,6 +136,31 @@ 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') @@ -142,6 +174,7 @@ def test_route_optional_slash(): request, response = app.test_client.get('/get/') assert response.text == 'OK' + def test_shorthand_routes_post(): app = Sanic('test_shorhand_routes_post') @@ -155,6 +188,7 @@ def test_shorthand_routes_post(): request, response = app.test_client.get('/post') assert response.status == 405 + def test_shorthand_routes_put(): app = Sanic('test_shorhand_routes_put') @@ -171,6 +205,7 @@ def test_shorthand_routes_put(): request, response = app.test_client.get('/put') assert response.status == 405 + def test_shorthand_routes_delete(): app = Sanic('test_shorhand_routes_delete') @@ -187,6 +222,7 @@ def test_shorthand_routes_delete(): request, response = app.test_client.get('/delete') assert response.status == 405 + def test_shorthand_routes_patch(): app = Sanic('test_shorhand_routes_patch') @@ -203,6 +239,7 @@ def test_shorthand_routes_patch(): request, response = app.test_client.get('/patch') assert response.status == 405 + def test_shorthand_routes_head(): app = Sanic('test_shorhand_routes_head') @@ -219,6 +256,7 @@ def test_shorthand_routes_head(): request, response = app.test_client.get('/head') assert response.status == 405 + def test_shorthand_routes_options(): app = Sanic('test_shorhand_routes_options') @@ -235,6 +273,7 @@ def test_shorthand_routes_options(): request, response = app.test_client.get('/options') assert response.status == 405 + def test_static_routes(): app = Sanic('test_dynamic_route') @@ -717,6 +756,7 @@ def test_remove_inexistent_route(): with pytest.raises(RouteDoesNotExist): app.remove_route('/test') + def test_removing_slash(): app = Sanic(__name__) @@ -835,7 +875,6 @@ def test_unmergeable_overload_routes(): request, response = app.test_client.post('/overload_whole') assert response.text == 'OK1' - @app.route('/overload_part', methods=['GET']) async def handler1(request): return text('OK1') @@ -850,3 +889,21 @@ def test_unmergeable_overload_routes(): request, response = app.test_client.post('/overload_part') assert response.status == 405 + + +def test_unicode_routes(): + app = Sanic('test_unicode_routes') + + @app.get('/你好') + def handler1(request): + return text('OK1') + + request, response = app.test_client.get('/你好') + assert response.text == 'OK1' + + @app.route('/overload/', methods=['GET']) + async def handler2(request, param): + return text('OK2 ' + param) + + request, response = app.test_client.get('/overload/你好') + assert response.text == 'OK2 你好' 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/tests/test_worker.py b/tests/test_worker.py index e2b301ec..af00982e 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -131,5 +131,5 @@ def test_worker_close(worker): loop.run_until_complete(_close) assert worker.signal.stopped == True - conn.websocket.close_connection.assert_called_with(force=True) + conn.websocket.close_connection.assert_called_with(after_handshake=False) assert len(worker.servers) == 0 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