This commit is contained in:
David Tan 2017-11-18 02:00:13 -05:00
commit 07d54455f5
18 changed files with 155 additions and 37 deletions

View File

@ -75,6 +75,10 @@ The following variables are accessible as properties on `Request` objects:
- `ip` (str) - IP address of the requester. - `ip` (str) - IP address of the requester.
- `port` (str) - Port address of the requester.
- `socket` (tuple) - (IP, port) of the requester.
- `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object. - `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object.
```python ```python

49
examples/pytest_xdist.py Normal file
View File

@ -0,0 +1,49 @@
"""pytest-xdist example for sanic server
Install testing tools:
$ pip install pytest pytest-xdist
Run with xdist params:
$ pytest examples/pytest_xdist.py -n 8 # 8 workers
"""
import re
from sanic import Sanic
from sanic.response import text
from sanic.testing import PORT as PORT_BASE, SanicTestClient
import pytest
@pytest.fixture(scope="session")
def test_port(worker_id):
m = re.search(r'[0-9]+', worker_id)
if m:
num_id = m.group(0)
else:
num_id = 0
port = PORT_BASE + int(num_id)
return port
@pytest.fixture(scope="session")
def app():
app = Sanic()
@app.route('/')
async def index(request):
return text('OK')
return app
@pytest.fixture(scope="session")
def client(app, test_port):
return SanicTestClient(app, test_port)
@pytest.mark.parametrize('run_id', range(100))
def test_index(client, run_id):
request, response = client._sanic_endpoint_test('get', '/')
assert response.status == 200
assert response.text == 'OK'

View File

@ -28,7 +28,8 @@ class Sanic:
def __init__(self, name=None, router=None, error_handler=None, def __init__(self, name=None, router=None, error_handler=None,
load_env=True, request_class=None, load_env=True, request_class=None,
strict_slashes=False, log_config=None): strict_slashes=False, log_config=None,
configure_logging=True):
# Get name from previous stack frame # Get name from previous stack frame
if name is None: if name is None:
@ -36,6 +37,7 @@ class Sanic:
name = getmodulename(frame_records[1]) name = getmodulename(frame_records[1])
# logging # logging
if configure_logging:
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
self.name = name self.name = name
@ -47,6 +49,7 @@ class Sanic:
self.response_middleware = deque() self.response_middleware = deque()
self.blueprints = {} self.blueprints = {}
self._blueprint_order = [] self._blueprint_order = []
self.configure_logging = configure_logging
self.debug = None self.debug = None
self.sock = None self.sock = None
self.strict_slashes = strict_slashes self.strict_slashes = strict_slashes
@ -345,13 +348,14 @@ class Sanic:
# Static Files # Static Files
def static(self, uri, file_or_directory, pattern=r'/?.+', def static(self, uri, file_or_directory, pattern=r'/?.+',
use_modified_since=True, use_content_range=False, use_modified_since=True, use_content_range=False,
stream_large_files=False, name='static', host=None): stream_large_files=False, name='static', host=None,
strict_slashes=None):
"""Register a root to serve files from. The input can either be a """Register a root to serve files from. The input can either be a
file or a directory. See file or a directory. See
""" """
static_register(self, uri, file_or_directory, pattern, static_register(self, uri, file_or_directory, pattern,
use_modified_since, use_content_range, use_modified_since, use_content_range,
stream_large_files, name, host) stream_large_files, name, host, strict_slashes)
def blueprint(self, blueprint, **options): def blueprint(self, blueprint, **options):
"""Register a blueprint on the application. """Register a blueprint on the application.
@ -574,9 +578,9 @@ class Sanic:
try: try:
response = await self._run_response_middleware(request, response = await self._run_response_middleware(request,
response) response)
except: except BaseException:
error_logger.exception( error_logger.exception(
'Exception occured in one of response middleware handlers' 'Exception occurred in one of response middleware handlers'
) )
# pass the response to the correct callback # pass the response to the correct callback
@ -642,7 +646,7 @@ class Sanic:
serve(**server_settings) serve(**server_settings)
else: else:
serve_multiple(server_settings, workers) serve_multiple(server_settings, workers)
except: except BaseException:
error_logger.exception( error_logger.exception(
'Experienced exception while trying to serve') 'Experienced exception while trying to serve')
raise raise
@ -793,7 +797,7 @@ class Sanic:
listeners = [partial(listener, self) for listener in listeners] listeners = [partial(listener, self) for listener in listeners]
server_settings[settings_name] = listeners server_settings[settings_name] = listeners
if debug: if self.configure_logging and debug:
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
if self.config.LOGO is not None: if self.config.LOGO is not None:
logger.debug(self.config.LOGO) logger.debug(self.config.LOGO)

View File

@ -221,8 +221,12 @@ class Blueprint:
name = kwargs.pop('name', 'static') name = kwargs.pop('name', 'static')
if not name.startswith(self.name + '.'): if not name.startswith(self.name + '.'):
name = '{}.{}'.format(self.name, name) name = '{}.{}'.format(self.name, name)
kwargs.update(name=name) kwargs.update(name=name)
strict_slashes = kwargs.get('strict_slashes')
if strict_slashes is None and self.strict_slashes is not None:
kwargs.update(strict_slashes=self.strict_slashes)
static = FutureStatic(uri, file_or_directory, args, kwargs) static = FutureStatic(uri, file_or_directory, args, kwargs)
self.statics.append(static) self.statics.append(static)

View File

@ -46,7 +46,8 @@ class Request(dict):
__slots__ = ( __slots__ = (
'app', 'headers', 'version', 'method', '_cookies', 'transport', 'app', 'headers', 'version', 'method', '_cookies', 'transport',
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', 'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
'_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr' '_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr',
'_socket', '_port'
) )
def __init__(self, url_bytes, headers, version, method, transport): def __init__(self, url_bytes, headers, version, method, transport):
@ -167,11 +168,27 @@ class Request(dict):
@property @property
def ip(self): def ip(self):
if not hasattr(self, '_ip'): if not hasattr(self, '_socket'):
self._ip = (self.transport.get_extra_info('peername') or self._get_address()
(None, None))
return self._ip return self._ip
@property
def port(self):
if not hasattr(self, '_socket'):
self._get_address()
return self._port
@property
def socket(self):
if not hasattr(self, '_socket'):
self._get_socket()
return self._socket
def _get_address(self):
self._socket = (self.transport.get_extra_info('peername') or
(None, None))
self._ip, self._port = self._socket
@property @property
def remote_addr(self): def remote_addr(self):
"""Attempt to return the original client ip based on X-Forwarded-For. """Attempt to return the original client ip based on X-Forwarded-For.

View File

@ -3,7 +3,7 @@ from os import path
try: try:
from ujson import dumps as json_dumps from ujson import dumps as json_dumps
except: except BaseException:
from json import dumps as json_dumps from json import dumps as json_dumps
from aiofiles import open as open_async from aiofiles import open as open_async

View File

@ -130,8 +130,15 @@ class Router:
return return
# Add versions with and without trailing / # Add versions with and without trailing /
slashed_methods = self.routes_all.get(uri + '/', frozenset({}))
if isinstance(methods, Iterable):
_slash_is_missing = all(method in slashed_methods for
method in methods)
else:
_slash_is_missing = methods in slashed_methods
slash_is_missing = ( slash_is_missing = (
not uri[-1] == '/' and not self.routes_all.get(uri + '/', False) not uri[-1] == '/' and not _slash_is_missing
) )
without_slash_is_missing = ( without_slash_is_missing = (
uri[-1] == '/' and not uri[-1] == '/' and not

View File

@ -588,7 +588,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
try: try:
http_server = loop.run_until_complete(server_coroutine) http_server = loop.run_until_complete(server_coroutine)
except: except BaseException:
logger.exception("Unable to start server") logger.exception("Unable to start server")
return return

View File

@ -18,7 +18,8 @@ from sanic.response import file, file_stream, HTTPResponse
def register(app, uri, file_or_directory, pattern, def register(app, uri, file_or_directory, pattern,
use_modified_since, use_content_range, use_modified_since, use_content_range,
stream_large_files, name='static', host=None): stream_large_files, name='static', host=None,
strict_slashes=None):
# TODO: Though sanic is not a file server, I feel like we should at least # 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 # make a good effort here. Modified-since is nice, but we could
# also look into etags, expires, and caching # also look into etags, expires, and caching
@ -103,7 +104,7 @@ def register(app, uri, file_or_directory, pattern,
if isinstance(stream_large_files, int): if isinstance(stream_large_files, int):
threshold = stream_large_files threshold = stream_large_files
else: else:
threshold = 1024*1000 threshold = 1024 * 1024
if not stats: if not stats:
stats = await stat(file_path) stats = await stat(file_path)
@ -122,4 +123,5 @@ def register(app, uri, file_or_directory, pattern,
if not name.startswith('_static_'): if not name.startswith('_static_'):
name = '_static_{}'.format(name) name = '_static_{}'.format(name)
app.route(uri, methods=['GET', 'HEAD'], name=name, host=host)(_handler) app.route(uri, methods=['GET', 'HEAD'], name=name, host=host,
strict_slashes=strict_slashes)(_handler)

View File

@ -8,8 +8,9 @@ PORT = 42101
class SanicTestClient: class SanicTestClient:
def __init__(self, app): def __init__(self, app, port=PORT):
self.app = app self.app = app
self.port = port
async def _local_request(self, method, uri, cookies=None, *args, **kwargs): async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
import aiohttp import aiohttp
@ -17,7 +18,7 @@ class 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)
logger.info(url) logger.info(url)
conn = aiohttp.TCPConnector(verify_ssl=False) conn = aiohttp.TCPConnector(verify_ssl=False)
@ -66,7 +67,7 @@ class SanicTestClient:
exceptions.append(e) exceptions.append(e)
self.app.stop() self.app.stop()
self.app.run(host=HOST, debug=debug, port=PORT, **server_kwargs) self.app.run(host=HOST, debug=debug, port=self.port, **server_kwargs)
self.app.listeners['after_server_start'].pop() self.app.listeners['after_server_start'].pop()
if exceptions: if exceptions:
@ -76,14 +77,14 @@ class SanicTestClient:
try: try:
request, response = results request, response = results
return request, response return request, response
except: except BaseException:
raise ValueError( raise ValueError(
"Request and response object expected, got ({})".format( "Request and response object expected, got ({})".format(
results)) results))
else: else:
try: try:
return results[-1] return results[-1]
except: except BaseException:
raise ValueError( raise ValueError(
"Request object expected, got ({})".format(results)) "Request object expected, got ({})".format(results))

View File

@ -90,4 +90,5 @@ class WebSocketProtocol(HttpProtocol):
) )
self.websocket.subprotocol = subprotocol self.websocket.subprotocol = subprotocol
self.websocket.connection_made(request.transport) self.websocket.connection_made(request.transport)
self.websocket.connection_open()
return self.websocket return self.websocket

View File

@ -74,13 +74,13 @@ class GunicornWorker(base.Worker):
trigger_events(self._server_settings.get('before_stop', []), trigger_events(self._server_settings.get('before_stop', []),
self.loop) self.loop)
self.loop.run_until_complete(self.close()) self.loop.run_until_complete(self.close())
except: except BaseException:
traceback.print_exc() traceback.print_exc()
finally: finally:
try: try:
trigger_events(self._server_settings.get('after_stop', []), trigger_events(self._server_settings.get('after_stop', []),
self.loop) self.loop)
except: except BaseException:
traceback.print_exc() traceback.print_exc()
finally: finally:
self.loop.close() self.loop.close()

View File

@ -58,11 +58,11 @@ def exception_app():
raise InvalidUsage("OK") raise InvalidUsage("OK")
@app.route('/abort/401') @app.route('/abort/401')
def handler_invalid(request): def handler_401_error(request):
abort(401) abort(401)
@app.route('/abort') @app.route('/abort')
def handler_invalid(request): def handler_500_error(request):
abort(500) abort(500)
return text("OK") return text("OK")
@ -186,7 +186,7 @@ def test_exception_in_exception_handler_debug_off(exception_app):
assert response.body == b'An error occurred while handling an error' assert response.body == b'An error occurred while handling an error'
def test_exception_in_exception_handler_debug_off(exception_app): def test_exception_in_exception_handler_debug_on(exception_app):
"""Test that an exception thrown in an error handler is handled""" """Test that an exception thrown in an error handler is handled"""
request, response = exception_app.test_client.get( request, response = exception_app.test_client.get(
'/error_in_error_handler_handler', '/error_in_error_handler_handler',

View File

@ -59,7 +59,7 @@ def test_middleware_response_exception():
result = {'status_code': None} result = {'status_code': None}
@app.middleware('response') @app.middleware('response')
async def process_response(reqest, response): async def process_response(request, response):
result['status_code'] = response.status result['status_code'] = response.status
return response return response

View File

@ -27,6 +27,16 @@ def test_sync():
assert response.text == 'Hello' assert response.text == 'Hello'
def test_remote_address():
app = Sanic('test_text')
@app.route('/')
def handler(request):
return text("{}".format(request.ip))
request, response = app.test_client.get('/')
assert response.text == '127.0.0.1'
def test_text(): def test_text():
app = Sanic('test_text') app = Sanic('test_text')

View File

@ -44,6 +44,24 @@ 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():
app = Sanic('test_shorthand_routes_multiple')
@app.get('/get')
def get_handler(request):
return text('OK')
@app.options('/get')
def options_handler(request):
return text('')
request, response = app.test_client.get('/get/')
assert response.status == 200
assert response.text == 'OK'
request, response = app.test_client.options('/get/')
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')
@ -754,6 +772,7 @@ def test_remove_route_without_clean_cache():
assert response.status == 200 assert response.status == 200
app.remove_route('/test', clean_cache=True) app.remove_route('/test', clean_cache=True)
app.remove_route('/test/', clean_cache=True)
request, response = app.test_client.get('/test') request, response = app.test_client.get('/test')
assert response.status == 404 assert response.status == 404

View File

@ -164,7 +164,7 @@ def test_static_content_range_error(file_name, static_file_directory):
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
def test_static_file(static_file_directory, file_name): def test_static_file_specified_host(static_file_directory, file_name):
app = Sanic('test_static') app = Sanic('test_static')
app.static( app.static(
'/testing.file', '/testing.file',

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 from sanic.testing import PORT as test_port, 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='localhost:{}'.format(test_port), _external=True) _server='{}:{}'.format(test_host, test_port), _external=True)
URL_FOR_VALUE3 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(test_port) URL_FOR_VALUE3 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port)
URL_FOR_ARGS4 = dict(arg1='v1', _anchor='anchor', _external=True, URL_FOR_ARGS4 = dict(arg1='v1', _anchor='anchor', _external=True,
_server='http://localhost:{}'.format(test_port),) _server='http://{}:{}'.format(test_host, test_port),)
URL_FOR_VALUE4 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(test_port) URL_FOR_VALUE4 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port)
def _generate_handlers_from_names(app, l): def _generate_handlers_from_names(app, l):