Merge branch 'master' of https://github.com/channelcat/sanic
This commit is contained in:
commit
07d54455f5
|
@ -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
49
examples/pytest_xdist.py
Normal 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'
|
20
sanic/app.py
20
sanic/app.py
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -90,4 +90,5 @@ class WebSocketProtocol(HttpProtocol):
|
|||
)
|
||||
self.websocket.subprotocol = subprotocol
|
||||
self.websocket.connection_made(request.transport)
|
||||
self.websocket.connection_open()
|
||||
return self.websocket
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue
Block a user