Merge pull request #16 from channelcat/master

Merge upstream master branch
This commit is contained in:
7 2018-01-15 10:48:34 -08:00 committed by GitHub
commit bd7333723e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 279 additions and 74 deletions

View File

@ -110,3 +110,23 @@ async def notify_server_started_after_five_seconds():
app.add_task(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))
`

View File

@ -20,7 +20,7 @@ def test_index_put_not_allowed():
assert response.status == 405 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`. your test request is executed against your application, using `aiohttp`.
The `test_client` methods accept the following arguments and keyword arguments: The `test_client` methods accept the following arguments and keyword arguments:

View File

@ -16,4 +16,5 @@ dependencies:
- ujson>=1.35 - ujson>=1.35
- aiofiles>=0.3.0 - aiofiles>=0.3.0
- websockets>=3.2 - websockets>=3.2
- sphinxcontrib-asyncio>=0.2.0
- https://github.com/channelcat/docutils-fork/zipball/master - https://github.com/channelcat/docutils-fork/zipball/master

View File

@ -1,6 +1,6 @@
from sanic.app import Sanic from sanic.app import Sanic
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
__version__ = '0.6.0' __version__ = '0.7.0'
__all__ = ['Sanic', 'Blueprint'] __all__ = ['Sanic', 'Blueprint']

View File

@ -86,9 +86,21 @@ class Sanic:
:param task: future, couroutine or awaitable :param task: future, couroutine or awaitable
""" """
try:
if callable(task):
try:
self.loop.create_task(task(self))
except TypeError:
self.loop.create_task(task())
else:
self.loop.create_task(task)
except SanicException:
@self.listener('before_server_start') @self.listener('before_server_start')
def run(app, loop): def run(app, loop):
if callable(task): if callable(task):
try:
loop.create_task(task(self))
except TypeError:
loop.create_task(task()) loop.create_task(task())
else: else:
loop.create_task(task) loop.create_task(task)
@ -544,6 +556,7 @@ class Sanic:
# Fetch handler from router # Fetch handler from router
handler, args, kwargs, uri = self.router.get(request) handler, args, kwargs, uri = self.router.get(request)
request.uri_template = uri request.uri_template = uri
if handler is None: if handler is None:
raise ServerError( raise ServerError(
@ -564,13 +577,17 @@ class Sanic:
if isawaitable(response): if isawaitable(response):
response = await response response = await response
except Exception as e: 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( response = HTTPResponse(
"Error while handling error: {}\nStack: {}".format( "Error while handling error: {}\nStack: {}".format(
e, format_exc())) e, format_exc()), status=500)
else: else:
response = HTTPResponse( response = HTTPResponse(
"An error occurred while handling an error") "An error occurred while handling an error",
status=500)
finally: finally:
# -------------------------------------------- # # -------------------------------------------- #
# Response Middleware # Response Middleware

View File

@ -83,6 +83,7 @@ class Cookie(dict):
"secure": "Secure", "secure": "Secure",
"httponly": "HttpOnly", "httponly": "HttpOnly",
"version": "Version", "version": "Version",
"samesite": "SameSite",
} }
_flags = {'secure', 'httponly'} _flags = {'secure', 'httponly'}

View File

@ -150,6 +150,16 @@ class InvalidUsage(SanicException):
pass 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) @add_status_code(500)
class ServerError(SanicException): class ServerError(SanicException):
pass pass
@ -167,8 +177,6 @@ class URLBuildError(ServerError):
class FileNotFound(NotFound): class FileNotFound(NotFound):
pass
def __init__(self, message, path, relative_url): def __init__(self, message, path, relative_url):
super().__init__(message) super().__init__(message)
self.path = path self.path = path
@ -198,8 +206,6 @@ class HeaderNotFound(InvalidUsage):
@add_status_code(416) @add_status_code(416)
class ContentRangeError(SanicException): class ContentRangeError(SanicException):
pass
def __init__(self, message, content_range): def __init__(self, message, content_range):
super().__init__(message) super().__init__(message)
self.headers = { self.headers = {
@ -257,7 +263,7 @@ class Unauthorized(SanicException):
# if auth-scheme is specified, set "WWW-Authenticate" header # if auth-scheme is specified, set "WWW-Authenticate" header
if scheme is not None: 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) challenge = ', '.join(values)
self.headers = { self.headers = {

View File

@ -1,5 +1,6 @@
import sys import sys
import json import json
import socket
from cgi import parse_header from cgi import parse_header
from collections import namedtuple from collections import namedtuple
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
@ -181,13 +182,22 @@ class Request(dict):
@property @property
def socket(self): def socket(self):
if not hasattr(self, '_socket'): if not hasattr(self, '_socket'):
self._get_socket() self._get_address()
return self._socket return self._socket
def _get_address(self): def _get_address(self):
sock = self.transport.get_extra_info('socket')
if sock.family == socket.AF_INET:
self._socket = (self.transport.get_extra_info('peername') or self._socket = (self.transport.get_extra_info('peername') or
(None, None)) (None, None))
self._ip, self._port = self._socket 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 @property
def remote_addr(self): def remote_addr(self):

View File

@ -2,8 +2,9 @@ import re
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from collections.abc import Iterable from collections.abc import Iterable
from functools import lru_cache 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 from sanic.views import CompositionView
Route = namedtuple( Route = namedtuple(
@ -129,18 +130,22 @@ class Router:
# Add versions with and without trailing / # Add versions with and without trailing /
slashed_methods = self.routes_all.get(uri + '/', frozenset({})) slashed_methods = self.routes_all.get(uri + '/', frozenset({}))
unslashed_methods = self.routes_all.get(uri[:-1], frozenset({}))
if isinstance(methods, Iterable): if isinstance(methods, Iterable):
_slash_is_missing = all(method in slashed_methods for _slash_is_missing = all(method in slashed_methods for
method in methods) method in methods)
_without_slash_is_missing = all(method in unslashed_methods for
method in methods)
else: else:
_slash_is_missing = methods in slashed_methods _slash_is_missing = methods in slashed_methods
_without_slash_is_missing = methods in unslashed_methods
slash_is_missing = ( slash_is_missing = (
not uri[-1] == '/' and not _slash_is_missing not uri[-1] == '/' and not _slash_is_missing
) )
without_slash_is_missing = ( without_slash_is_missing = (
uri[-1] == '/' and not uri[-1] == '/' and not
self.routes_all.get(uri[:-1], False) and not _without_slash_is_missing and not
uri == '/' uri == '/'
) )
# add version with trailing slash # add version with trailing slash
@ -350,6 +355,16 @@ class Router:
except NotFound: except NotFound:
return self._get(request.path, request.method, '') 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) @lru_cache(maxsize=ROUTER_CACHE_SIZE)
def _get(self, url, method, host): def _get(self, url, method, host):
"""Get a request handler based on the URL of the request, or raises an """Get a request handler based on the URL of the request, or raises an
@ -359,12 +374,13 @@ class Router:
:param method: request method :param method: request method
:return: handler, arguments, keyword arguments :return: handler, arguments, keyword arguments
""" """
url = host + url url = unquote(host + url)
# Check against known static routes # Check against known static routes
route = self.routes_static.get(url) route = self.routes_static.get(url)
method_not_supported = InvalidUsage( method_not_supported = MethodNotSupported(
'Method {} not allowed for URL {}'.format( 'Method {} not allowed for URL {}'.format(method, url),
method, url), status_code=405) method=method,
allowed_methods=self.get_supported_methods(url))
if route: if route:
if route.methods and method not in route.methods: if route.methods and method not in route.methods:
raise method_not_supported raise method_not_supported
@ -407,7 +423,7 @@ class Router:
""" """
try: try:
handler = self.get(request)[0] handler = self.get(request)[0]
except (NotFound, InvalidUsage): except (NotFound, MethodNotSupported):
return False return False
if (hasattr(handler, 'view_class') and if (hasattr(handler, 'view_class') and
hasattr(handler.view_class, request.method.lower())): hasattr(handler.view_class, request.method.lower())):

View File

@ -174,6 +174,10 @@ class HttpProtocol(asyncio.Protocol):
self.response_timeout_callback) self.response_timeout_callback)
) )
else: else:
if self._request_stream_task:
self._request_stream_task.cancel()
if self._request_handler_task:
self._request_handler_task.cancel()
try: try:
raise ServiceUnavailable('Response Timeout') raise ServiceUnavailable('Response Timeout')
except ServiceUnavailable as exception: except ServiceUnavailable as exception:
@ -312,13 +316,15 @@ class HttpProtocol(asyncio.Protocol):
else: else:
extra['byte'] = -1 extra['byte'] = -1
extra['host'] = 'UNKNOWN'
if self.request is not None: if self.request is not None:
extra['host'] = '{0}:{1}'.format(self.request.ip[0], if self.request.ip:
self.request.ip[1]) extra['host'] = '{0}:{1}'.format(self.request.ip,
self.request.port)
extra['request'] = '{0} {1}'.format(self.request.method, extra['request'] = '{0} {1}'.format(self.request.method,
self.request.url) self.request.url)
else: else:
extra['host'] = 'UNKNOWN'
extra['request'] = 'nil' extra['request'] = 'nil'
access_logger.info('', extra=extra) access_logger.info('', extra=extra)
@ -426,7 +432,10 @@ class HttpProtocol(asyncio.Protocol):
if self.parser and (self.keep_alive if self.parser and (self.keep_alive
or getattr(response, 'status', 0) == 408): or getattr(response, 'status', 0) == 408):
self.log_response(response) self.log_response(response)
try:
self.transport.close() self.transport.close()
except AttributeError as e:
logger.debug('Connection lost before server could close it.')
def bail_out(self, message, from_error=False): def bail_out(self, message, from_error=False):
if from_error or self.transport.is_closing(): if from_error or self.transport.is_closing():
@ -635,7 +644,9 @@ def serve(host, port, request_handler, error_handler, before_start=None,
coros = [] coros = []
for conn in connections: for conn in connections:
if hasattr(conn, "websocket") and conn.websocket: 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: else:
conn.close() conn.close()

View File

@ -115,7 +115,9 @@ class GunicornWorker(base.Worker):
coros = [] coros = []
for conn in self.connections: for conn in self.connections:
if hasattr(conn, "websocket") and conn.websocket: 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: else:
conn.close() conn.close()
_shutdown = asyncio.gather(*coros, loop=self.loop) _shutdown = asyncio.gather(*coros, loop=self.loop)

View File

@ -59,13 +59,14 @@ requirements = [
uvloop, uvloop,
ujson, ujson,
'aiofiles>=0.3.0', 'aiofiles>=0.3.0',
'websockets>=3.2', 'websockets>=4.0',
] ]
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")): if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
print("Installing without uJSON") print("Installing without uJSON")
requirements.remove(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") print("Installing without uvLoop")
requirements.remove(uvloop) requirements.remove(uvloop)

23
tests/conftest.py Normal file
View File

@ -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

View File

@ -2,6 +2,7 @@ from sanic import Sanic
from sanic.response import text from sanic.response import text
from threading import Event from threading import Event
import asyncio import asyncio
from queue import Queue
def test_create_task(): def test_create_task():
@ -28,3 +29,19 @@ def test_create_task():
request, response = app.test_client.get('/late') request, response = app.test_client.get('/late')
assert response.body == b'True' 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'

View File

@ -138,7 +138,7 @@ def test_unauthorized_exception(exception_app):
request, response = exception_app.test_client.get('/401/basic') request, response = exception_app.test_client.get('/401/basic')
assert response.status == 401 assert response.status == 401
assert response.headers.get('WWW-Authenticate') is not None 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') request, response = exception_app.test_client.get('/401/digest')
assert response.status == 401 assert response.status == 401
@ -146,10 +146,10 @@ def test_unauthorized_exception(exception_app):
auth_header = response.headers.get('WWW-Authenticate') auth_header = response.headers.get('WWW-Authenticate')
assert auth_header is not None assert auth_header is not None
assert auth_header.startswith('Digest') assert auth_header.startswith('Digest')
assert "qop='auth, auth-int'" in auth_header assert 'qop="auth, auth-int"' in auth_header
assert "algorithm='MD5'" in auth_header assert 'algorithm="MD5"' in auth_header
assert "nonce='abcdef'" in auth_header assert 'nonce="abcdef"' in auth_header
assert "opaque='zyxwvu'" in auth_header assert 'opaque="zyxwvu"' in auth_header
request, response = exception_app.test_client.get('/401/bearer') request, response = exception_app.test_client.get('/401/bearer')
assert response.status == 401 assert response.status == 401

View File

@ -7,7 +7,7 @@ from sanic.config import Config
from sanic import server from sanic import server
import aiohttp import aiohttp
from aiohttp import TCPConnector from aiohttp import TCPConnector
from sanic.testing import SanicTestClient, HOST, PORT from sanic.testing import SanicTestClient, HOST
class ReuseableTCPConnector(TCPConnector): class ReuseableTCPConnector(TCPConnector):
@ -30,7 +30,7 @@ class ReuseableTCPConnector(TCPConnector):
class ReuseableSanicTestClient(SanicTestClient): class ReuseableSanicTestClient(SanicTestClient):
def __init__(self, app, loop=None): def __init__(self, app, loop=None):
super(ReuseableSanicTestClient, self).__init__(app) super().__init__(app, port=app.test_port)
if loop is None: if loop is None:
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
self._loop = loop self._loop = loop
@ -68,13 +68,14 @@ class ReuseableSanicTestClient(SanicTestClient):
import traceback import traceback
traceback.print_tb(e2.__traceback__) traceback.print_tb(e2.__traceback__)
exceptions.append(e2) exceptions.append(e2)
#Don't stop here! self.app.stop() # Don't stop here! self.app.stop()
if self._server is not None: if self._server is not None:
_server = self._server _server = self._server
else: else:
_server_co = self.app.create_server(host=HOST, debug=debug, _server_co = self.app.create_server(host=HOST, debug=debug,
port=PORT, **server_kwargs) port=self.app.test_port,
**server_kwargs)
server.trigger_events( server.trigger_events(
self.app.listeners['before_server_start'], loop) self.app.listeners['before_server_start'], loop)
@ -133,7 +134,7 @@ class ReuseableSanicTestClient(SanicTestClient):
url = uri url = uri
else: else:
url = 'http://{host}:{port}{uri}'.format( 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) do_kill_session = kwargs.pop('end_session', False)
if self._session: if self._session:
session = self._session session = self._session

View File

@ -101,7 +101,6 @@ def test_log_connection_lost(debug, monkeypatch):
log = stream.getvalue() log = stream.getvalue()
if debug: if debug:
assert log.startswith( assert 'Connection lost before response written @' in log
'Connection lost before response written @')
else: else:
assert 'Connection lost before response written @' not in log assert 'Connection lost before response written @' not in log

View File

@ -3,7 +3,7 @@ import random
import signal import signal
from sanic import Sanic from sanic import Sanic
from sanic.testing import HOST, PORT from sanic.testing import HOST
def test_multiprocessing(): def test_multiprocessing():
@ -20,7 +20,7 @@ def test_multiprocessing():
signal.signal(signal.SIGALRM, stop_on_alarm) signal.signal(signal.SIGALRM, stop_on_alarm)
signal.alarm(1) 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 assert len(process_list) == num_workers

View File

@ -6,7 +6,7 @@ from sanic.response import text
from sanic.config import Config from sanic.config import Config
import aiohttp import aiohttp
from aiohttp import TCPConnector from aiohttp import TCPConnector
from sanic.testing import SanicTestClient, HOST, PORT from sanic.testing import SanicTestClient, HOST
class DelayableTCPConnector(TCPConnector): class DelayableTCPConnector(TCPConnector):
@ -96,7 +96,7 @@ class DelayableTCPConnector(TCPConnector):
class DelayableSanicTestClient(SanicTestClient): class DelayableSanicTestClient(SanicTestClient):
def __init__(self, app, loop, request_delay=1): 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._request_delay = request_delay
self._loop = None self._loop = None
@ -108,7 +108,7 @@ class DelayableSanicTestClient(SanicTestClient):
url = uri url = uri
else: else:
url = 'http://{host}:{port}{uri}'.format( 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, conn = DelayableTCPConnector(pre_request_delay=self._request_delay,
verify_ssl=False, loop=self._loop) verify_ssl=False, loop=self._loop)
async with aiohttp.ClientSession(cookies=cookies, connector=conn, async with aiohttp.ClientSession(cookies=cookies, connector=conn,

View File

@ -9,7 +9,7 @@ from sanic import Sanic
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
from sanic.response import json, text from sanic.response import json, text
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE 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) app.add_route(handler, path)
request, response = app.test_client.get(path + '?{}'.format(query)) 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) parsed = urlparse(request.url)
@ -369,9 +369,9 @@ def test_url_attributes_with_ssl(path, query, expected_url):
app.add_route(handler, path) app.add_route(handler, path)
request, response = app.test_client.get( 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}) 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) parsed = urlparse(request.url)

View File

@ -10,7 +10,7 @@ from random import choice
from sanic import Sanic from sanic import Sanic
from sanic.response import HTTPResponse, stream, StreamingHTTPResponse, file, file_stream, json 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 from unittest.mock import MagicMock
JSON_DATA = {'ok': True} JSON_DATA = {'ok': True}
@ -35,6 +35,25 @@ async def sample_streaming_fn(response):
await asyncio.sleep(.001) await asyncio.sleep(.001)
response.write('bar') 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 @pytest.fixture
def json_app(): def json_app():
@ -120,7 +139,7 @@ def test_stream_response_writes_correct_content_to_transport(streaming_app):
app.stop() app.stop()
streaming_app.run(host=HOST, port=PORT) streaming_app.run(host=HOST, port=streaming_app.test_port)
@pytest.fixture @pytest.fixture

View File

@ -29,7 +29,8 @@ def test_versioned_routes_get(method):
client_method = getattr(app.test_client, method) client_method = getattr(app.test_client, method)
request, response = client_method('/v1/{}'.format(method)) request, response = client_method('/v1/{}'.format(method))
assert response.status== 200 assert response.status == 200
def test_shorthand_routes_get(): def test_shorthand_routes_get():
app = Sanic('test_shorhand_routes_get') app = Sanic('test_shorhand_routes_get')
@ -44,6 +45,7 @@ def test_shorthand_routes_get():
request, response = app.test_client.post('/get') request, response = app.test_client.post('/get')
assert response.status == 405 assert response.status == 405
def test_shorthand_routes_multiple(): def test_shorthand_routes_multiple():
app = Sanic('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/') request, response = app.test_client.options('/get/')
assert response.status == 200 assert response.status == 200
def test_route_strict_slash(): def test_route_strict_slash():
app = Sanic('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') request, response = app.test_client.post('/post')
assert response.status == 404 assert response.status == 404
def test_route_invalid_parameter_syntax(): def test_route_invalid_parameter_syntax():
with pytest.raises(ValueError): with pytest.raises(ValueError):
app = Sanic('test_route_invalid_param_syntax') app = Sanic('test_route_invalid_param_syntax')
@ -99,6 +103,7 @@ def test_route_invalid_parameter_syntax():
request, response = app.test_client.get('/get') request, response = app.test_client.get('/get')
def test_route_strict_slash_default_value(): def test_route_strict_slash_default_value():
app = Sanic('test_route_strict_slash', strict_slashes=True) 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/') request, response = app.test_client.get('/get/')
assert response.status == 404 assert response.status == 404
def test_route_strict_slash_without_passing_default_value(): def test_route_strict_slash_without_passing_default_value():
app = Sanic('test_route_strict_slash') 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/') request, response = app.test_client.get('/get/')
assert response.text == 'OK' assert response.text == 'OK'
def test_route_strict_slash_default_value_can_be_overwritten(): def test_route_strict_slash_default_value_can_be_overwritten():
app = Sanic('test_route_strict_slash', strict_slashes=True) 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/') request, response = app.test_client.get('/get/')
assert response.text == 'OK' 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(): def test_route_optional_slash():
app = Sanic('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/') request, response = app.test_client.get('/get/')
assert response.text == 'OK' assert response.text == 'OK'
def test_shorthand_routes_post(): def test_shorthand_routes_post():
app = Sanic('test_shorhand_routes_post') app = Sanic('test_shorhand_routes_post')
@ -155,6 +188,7 @@ def test_shorthand_routes_post():
request, response = app.test_client.get('/post') request, response = app.test_client.get('/post')
assert response.status == 405 assert response.status == 405
def test_shorthand_routes_put(): def test_shorthand_routes_put():
app = Sanic('test_shorhand_routes_put') app = Sanic('test_shorhand_routes_put')
@ -171,6 +205,7 @@ def test_shorthand_routes_put():
request, response = app.test_client.get('/put') request, response = app.test_client.get('/put')
assert response.status == 405 assert response.status == 405
def test_shorthand_routes_delete(): def test_shorthand_routes_delete():
app = Sanic('test_shorhand_routes_delete') app = Sanic('test_shorhand_routes_delete')
@ -187,6 +222,7 @@ def test_shorthand_routes_delete():
request, response = app.test_client.get('/delete') request, response = app.test_client.get('/delete')
assert response.status == 405 assert response.status == 405
def test_shorthand_routes_patch(): def test_shorthand_routes_patch():
app = Sanic('test_shorhand_routes_patch') app = Sanic('test_shorhand_routes_patch')
@ -203,6 +239,7 @@ def test_shorthand_routes_patch():
request, response = app.test_client.get('/patch') request, response = app.test_client.get('/patch')
assert response.status == 405 assert response.status == 405
def test_shorthand_routes_head(): def test_shorthand_routes_head():
app = Sanic('test_shorhand_routes_head') app = Sanic('test_shorhand_routes_head')
@ -219,6 +256,7 @@ def test_shorthand_routes_head():
request, response = app.test_client.get('/head') request, response = app.test_client.get('/head')
assert response.status == 405 assert response.status == 405
def test_shorthand_routes_options(): def test_shorthand_routes_options():
app = Sanic('test_shorhand_routes_options') app = Sanic('test_shorhand_routes_options')
@ -235,6 +273,7 @@ def test_shorthand_routes_options():
request, response = app.test_client.get('/options') request, response = app.test_client.get('/options')
assert response.status == 405 assert response.status == 405
def test_static_routes(): def test_static_routes():
app = Sanic('test_dynamic_route') app = Sanic('test_dynamic_route')
@ -717,6 +756,7 @@ def test_remove_inexistent_route():
with pytest.raises(RouteDoesNotExist): with pytest.raises(RouteDoesNotExist):
app.remove_route('/test') app.remove_route('/test')
def test_removing_slash(): def test_removing_slash():
app = Sanic(__name__) app = Sanic(__name__)
@ -835,7 +875,6 @@ def test_unmergeable_overload_routes():
request, response = app.test_client.post('/overload_whole') request, response = app.test_client.post('/overload_whole')
assert response.text == 'OK1' assert response.text == 'OK1'
@app.route('/overload_part', methods=['GET']) @app.route('/overload_part', methods=['GET'])
async def handler1(request): async def handler1(request):
return text('OK1') return text('OK1')
@ -850,3 +889,21 @@ def test_unmergeable_overload_routes():
request, response = app.test_client.post('/overload_part') request, response = app.test_client.post('/overload_part')
assert response.status == 405 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/<param>', methods=['GET'])
async def handler2(request, param):
return text('OK2 ' + param)
request, response = app.test_client.get('/overload/你好')
assert response.text == 'OK2 你好'

View File

@ -6,7 +6,7 @@ import signal
import pytest import pytest
from sanic import Sanic from sanic import Sanic
from sanic.testing import HOST, PORT from sanic.testing import HOST
AVAILABLE_LISTENERS = [ AVAILABLE_LISTENERS = [
'before_server_start', 'before_server_start',
@ -31,7 +31,7 @@ def start_stop_app(random_name_app, **run_kwargs):
signal.signal(signal.SIGALRM, stop_on_alarm) signal.signal(signal.SIGALRM, stop_on_alarm)
signal.alarm(1) signal.alarm(1)
try: try:
random_name_app.run(HOST, PORT, **run_kwargs) random_name_app.run(HOST, random_name_app.test_port, **run_kwargs)
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass

View File

@ -1,8 +1,7 @@
from sanic import Sanic from sanic import Sanic
from sanic.response import HTTPResponse from sanic.response import HTTPResponse
from sanic.testing import HOST, PORT from sanic.testing import HOST
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest
import asyncio import asyncio
from queue import Queue from queue import Queue
@ -31,7 +30,7 @@ def test_register_system_signals():
app.listener('before_server_start')(set_loop) app.listener('before_server_start')(set_loop)
app.listener('after_server_stop')(after) app.listener('after_server_stop')(after)
app.run(HOST, PORT) app.run(HOST, app.test_port)
assert calledq.get() == True assert calledq.get() == True
@ -47,5 +46,5 @@ def test_dont_register_system_signals():
app.listener('before_server_start')(set_loop) app.listener('before_server_start')(set_loop)
app.listener('after_server_stop')(after) 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 assert calledq.get() == False

View File

@ -5,7 +5,7 @@ from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.views import HTTPMethodView from sanic.views import HTTPMethodView
from sanic.blueprints import Blueprint 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 from sanic.exceptions import URLBuildError
import string 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_ARGS2 = dict(arg1=['v1', 'v2'], _anchor='anchor')
URL_FOR_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor' URL_FOR_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor'
URL_FOR_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http', URL_FOR_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http',
_server='{}:{}'.format(test_host, test_port), _external=True) _server='{}:PORT_PLACEHOLDER'.format(test_host), _external=True)
URL_FOR_VALUE3 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port) URL_FOR_VALUE3 = 'http://{}:PORT_PLACEHOLDER/myurl?arg1=v1#anchor'.format(test_host)
URL_FOR_ARGS4 = dict(arg1='v1', _anchor='anchor', _external=True, URL_FOR_ARGS4 = dict(arg1='v1', _anchor='anchor', _external=True,
_server='http://{}:{}'.format(test_host, test_port),) _server='http://{}:PORT_PLACEHOLDER'.format(test_host),)
URL_FOR_VALUE4 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port) URL_FOR_VALUE4 = 'http://{}:PORT_PLACEHOLDER/myurl?arg1=v1#anchor'.format(test_host)
def _generate_handlers_from_names(app, l): 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): def passes(request):
return text('this should pass') 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) assert url == app.url_for('passes', **args)
request, response = app.test_client.get(url) request, response = app.test_client.get(url)
assert response.status == 200 assert response.status == 200

View File

@ -131,5 +131,5 @@ def test_worker_close(worker):
loop.run_until_complete(_close) loop.run_until_complete(_close)
assert worker.signal.stopped == True 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 assert len(worker.servers) == 0

View File

@ -12,12 +12,13 @@ deps =
pytest-cov pytest-cov
pytest-sanic pytest-sanic
pytest-sugar pytest-sugar
pytest-xdist
aiohttp==1.3.5 aiohttp==1.3.5
chardet<=2.3.0 chardet<=2.3.0
beautifulsoup4 beautifulsoup4
gunicorn gunicorn
commands = commands =
pytest tests --cov sanic --cov-report= {posargs} pytest tests -n 4 --cov sanic --cov-report= {posargs}
- coverage combine --append - coverage combine --append
coverage report -m coverage report -m