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

View File

@ -221,8 +221,12 @@ class Blueprint:
name = kwargs.pop('name', 'static')
if not name.startswith(self.name + '.'):
name = '{}.{}'.format(self.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)
self.statics.append(static)

View File

@ -46,7 +46,8 @@ class Request(dict):
__slots__ = (
'app', 'headers', 'version', 'method', '_cookies', 'transport',
'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):
@ -167,11 +168,27 @@ class Request(dict):
@property
def ip(self):
if not hasattr(self, '_ip'):
self._ip = (self.transport.get_extra_info('peername') or
(None, None))
if not hasattr(self, '_socket'):
self._get_address()
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
def remote_addr(self):
"""Attempt to return the original client ip based on X-Forwarded-For.

View File

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

View File

@ -130,8 +130,15 @@ class Router:
return
# 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 = (
not uri[-1] == '/' and not self.routes_all.get(uri + '/', False)
not uri[-1] == '/' and not _slash_is_missing
)
without_slash_is_missing = (
uri[-1] == '/' and not

View File

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

View File

@ -18,7 +18,8 @@ from sanic.response import file, file_stream, HTTPResponse
def register(app, uri, file_or_directory, pattern,
use_modified_since, use_content_range,
stream_large_files, name='static', host=None):
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
# make a good effort here. Modified-since is nice, but we could
# 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):
threshold = stream_large_files
else:
threshold = 1024*1000
threshold = 1024 * 1024
if not stats:
stats = await stat(file_path)
@ -122,4 +123,5 @@ def register(app, uri, file_or_directory, pattern,
if not name.startswith('_static_'):
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:
def __init__(self, app):
def __init__(self, app, port=PORT):
self.app = app
self.port = port
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
import aiohttp
@ -17,7 +18,7 @@ class SanicTestClient:
url = uri
else:
url = 'http://{host}:{port}{uri}'.format(
host=HOST, port=PORT, uri=uri)
host=HOST, port=self.port, uri=uri)
logger.info(url)
conn = aiohttp.TCPConnector(verify_ssl=False)
@ -66,7 +67,7 @@ class SanicTestClient:
exceptions.append(e)
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()
if exceptions:
@ -76,14 +77,14 @@ class SanicTestClient:
try:
request, response = results
return request, response
except:
except BaseException:
raise ValueError(
"Request and response object expected, got ({})".format(
results))
else:
try:
return results[-1]
except:
except BaseException:
raise ValueError(
"Request object expected, got ({})".format(results))

View File

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

View File

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

View File

@ -58,11 +58,11 @@ def exception_app():
raise InvalidUsage("OK")
@app.route('/abort/401')
def handler_invalid(request):
def handler_401_error(request):
abort(401)
@app.route('/abort')
def handler_invalid(request):
def handler_500_error(request):
abort(500)
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'
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"""
request, response = exception_app.test_client.get(
'/error_in_error_handler_handler',

View File

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

View File

@ -27,6 +27,16 @@ def test_sync():
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():
app = Sanic('test_text')

View File

@ -44,6 +44,24 @@ 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')
@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():
app = Sanic('test_route_strict_slash')
@ -431,7 +449,7 @@ def test_websocket_route_with_subprotocols():
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13'})
assert response.status == 101
assert results == ['bar', 'bar', None, None]
@ -754,6 +772,7 @@ def test_remove_route_without_clean_cache():
assert response.status == 200
app.remove_route('/test', clean_cache=True)
app.remove_route('/test/', clean_cache=True)
request, response = app.test_client.get('/test')
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'])
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.static(
'/testing.file',

View File

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