Merge pull request #14 from channelcat/master

merge upstream master branch
This commit is contained in:
7 2017-10-09 07:19:57 -07:00 committed by GitHub
commit 9bc1abcd00
35 changed files with 1479 additions and 387 deletions

View File

@ -25,7 +25,7 @@ import sanic
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
extensions = ['sphinx.ext.autodoc'] extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.asyncio']
templates_path = ['_templates'] templates_path = ['_templates']

View File

@ -93,7 +93,14 @@ def ignore_404s(request, exception):
Static files can be served globally, under the blueprint prefix. Static files can be served globally, under the blueprint prefix.
```python ```python
bp.static('/folder/to/serve', '/web/path')
# suppose bp.name == 'bp'
bp.static('/web/path', '/folder/to/serve')
# also you can pass name parameter to it for url_for
bp.static('/web/path', '/folder/to/server', name='uploads')
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/web/path/file.txt'
``` ```
## Start and stop ## Start and stop

View File

@ -7,6 +7,7 @@ A list of Sanic extensions created by the community.
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors. - [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress. - [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress.
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template. - [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
- [JWT](https://github.com/ahopkins/sanic-jwt): Authentication extension for JSON Web Tokens (JWT).
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI. - [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support. - [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper. - [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper.

View File

@ -9,15 +9,16 @@ syntax, so earlier versions of python won't work.
```python ```python
from sanic import Sanic from sanic import Sanic
from sanic.response import text from sanic.response import json
app = Sanic(__name__) app = Sanic()
@app.route("/") @app.route("/")
async def test(request): async def test(request):
return text('Hello world!') return json({"hello": "world"})
app.run(host="0.0.0.0", port=8000, debug=True) if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
``` ```
3. Run the server: `python3 main.py` 3. Run the server: `python3 main.py`

View File

@ -9,12 +9,6 @@ A simple example using default settings would be like this:
```python ```python
from sanic import Sanic from sanic import Sanic
from sanic.config import LOGGING
# The default logging handlers are ['accessStream', 'errorStream']
# but we change it to use other handlers here for demo purpose
LOGGING['loggers']['network']['handlers'] = [
'accessSysLog', 'errorSysLog']
app = Sanic('test') app = Sanic('test')
@ -23,14 +17,21 @@ async def test(request):
return response.text('Hello World!') return response.text('Hello World!')
if __name__ == "__main__": if __name__ == "__main__":
app.run(log_config=LOGGING) app.run(debug=True, access_log=True)
``` ```
And to close logging, simply assign log_config=None: To use your own logging config, simply use `logging.config.dictConfig`, or
pass `log_config` when you initialize `Sanic` app:
```python
app = Sanic('test', log_config=LOGGING_CONFIG)
```
And to close logging, simply assign access_log=False:
```python ```python
if __name__ == "__main__": if __name__ == "__main__":
app.run(log_config=None) app.run(access_log=False)
``` ```
This would skip calling logging functions when handling requests. This would skip calling logging functions when handling requests.
@ -38,64 +39,29 @@ And you could even do further in production to gain extra speed:
```python ```python
if __name__ == "__main__": if __name__ == "__main__":
# disable internal messages # disable debug messages
app.run(debug=False, log_config=None) app.run(debug=False, access_log=False)
``` ```
### Configuration ### Configuration
By default, log_config parameter is set to use sanic.config.LOGGING dictionary for configuration. The default configuration provides several predefined `handlers`: By default, log_config parameter is set to use sanic.log.LOGGING_CONFIG_DEFAULTS dictionary for configuration.
- internal (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br> There are three `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
For internal information console outputs.
- root:<br>
- accessStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br>
For requests information logging in console
- errorStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br>
For error message and traceback logging in console.
- accessSysLog (using [logging.handlers.SysLogHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler))<br>
For requests information logging to syslog.
Currently supports Windows (via localhost:514), Darwin (/var/run/syslog),
Linux (/dev/log) and FreeBSD (/dev/log).<br>
You would not be able to access this property if the directory doesn't exist.
(Notice that in Docker you have to enable everything by yourself)
- errorSysLog (using [logging.handlers.SysLogHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler))<br>
For error message and traceback logging to syslog.
Currently supports Windows (via localhost:514), Darwin (/var/run/syslog),
Linux (/dev/log) and FreeBSD (/dev/log).<br>
You would not be able to access this property if the directory doesn't exist.
(Notice that in Docker you have to enable everything by yourself)
And `filters`:
- accessFilter (using sanic.log.DefaultFilter)<br>
The filter that allows only levels in `DEBUG`, `INFO`, and `NONE(0)`
- errorFilter (using sanic.log.DefaultFilter)<br>
The filter that allows only levels in `WARNING`, `ERROR`, and `CRITICAL`
There are two `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
- sanic:<br>
Used to log internal messages. Used to log internal messages.
- sanic.error:<br>
Used to log error logs.
- network:<br> - sanic.access:<br>
Used to log requests from network, and any information from those requests. Used to log access logs.
#### Log format: #### Log format:
In addition to default parameters provided by python (asctime, levelname, message), In addition to default parameters provided by python (asctime, levelname, message),
Sanic provides additional parameters for network logger with accessFilter: Sanic provides additional parameters for access logger with:
- host (str)<br> - host (str)<br>
request.ip request.ip

View File

@ -301,3 +301,34 @@ def handler(request):
# app.url_for('handler') == '/get' # app.url_for('handler') == '/get'
# app.url_for('post_handler') == '/post' # app.url_for('post_handler') == '/post'
``` ```
## Build URL for static files
You can use `url_for` for static file url building now.
If it's for file directly, `filename` can be ignored.
```python
app = Sanic('test_static')
app.static('/static', './static')
app.static('/uploads', './uploads', name='uploads')
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
bp = Blueprint('bp', url_prefix='bp')
bp.static('/static', './static')
bp.static('/uploads', './uploads', name='uploads')
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
app.blueprint(bp)
# then build the url
app.url_for('static', filename='file.txt') == '/static/file.txt'
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
app.url_for('static', name='uploads', filename='file.txt') == '/uploads/file.txt'
app.url_for('static', name='best_png') == '/the_best.png'
# blueprint url building
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/uploads/file.txt'
app.url_for('static', name='bp.best_png') == '/bp/static/the_best.png'
```

View File

@ -6,16 +6,40 @@ filename. The file specified will then be accessible via the given endpoint.
```python ```python
from sanic import Sanic from sanic import Sanic
from sanic.blueprints import Blueprint
app = Sanic(__name__) app = Sanic(__name__)
# Serves files from the static folder to the URL /static # Serves files from the static folder to the URL /static
app.static('/static', './static') app.static('/static', './static')
# use url_for to build the url, name defaults to 'static' and can be ignored
app.url_for('static', filename='file.txt') == '/static/file.txt'
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
# Serves the file /home/ubuntu/test.png when the URL /the_best.png # Serves the file /home/ubuntu/test.png when the URL /the_best.png
# is requested # is requested
app.static('/the_best.png', '/home/ubuntu/test.png') app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
# you can use url_for to build the static file url
# you can ignore name and filename parameters if you don't define it
app.url_for('static', name='best_png') == '/the_best.png'
app.url_for('static', name='best_png', filename='any') == '/the_best.png'
# you need define the name for other static files
app.static('/another.png', '/home/ubuntu/another.png', name='another')
app.url_for('static', name='another') == '/another.png'
app.url_for('static', name='another', filename='any') == '/another.png'
# also, you can use static for blueprint
bp = Blueprint('bp', url_prefix='/bp')
bp.static('/static', './static')
# servers the file directly
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
app.blueprint(bp)
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
app.url_for('static', name='bp.best_png') == '/bp/test_best.png'
app.run(host="0.0.0.0", port=8000) app.run(host="0.0.0.0", port=8000)
``` ```
Note: currently you cannot build a URL for a static file using `url_for`.

13
examples/teapot.py Normal file
View File

@ -0,0 +1,13 @@
from sanic import Sanic
from sanic import response as res
app = Sanic(__name__)
@app.route("/")
async def test(req):
return res.text("I\'m a teapot", status=418)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@ -1,3 +1,4 @@
sphinx sphinx
sphinx_rtd_theme sphinx_rtd_theme
recommonmark recommonmark
sphinxcontrib-asyncio

View File

@ -1,7 +1,7 @@
from argparse import ArgumentParser from argparse import ArgumentParser
from importlib import import_module from importlib import import_module
from sanic.log import log from sanic.log import logger
from sanic.app import Sanic from sanic.app import Sanic
if __name__ == "__main__": if __name__ == "__main__":
@ -36,9 +36,9 @@ if __name__ == "__main__":
app.run(host=args.host, port=args.port, app.run(host=args.host, port=args.port,
workers=args.workers, debug=args.debug, ssl=ssl) workers=args.workers, debug=args.debug, ssl=ssl)
except ImportError as e: except ImportError as e:
log.error("No module named {} found.\n" logger.error("No module named {} found.\n"
" Example File: project/sanic_server.py -> app\n" " Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app" " Example Module: project.sanic_server.app"
.format(e.name)) .format(e.name))
except ValueError as e: except ValueError as e:
log.error("{}".format(e)) logger.error("{}".format(e))

View File

@ -10,11 +10,11 @@ from traceback import format_exc
from urllib.parse import urlencode, urlunparse from urllib.parse import urlencode, urlunparse
from ssl import create_default_context, Purpose from ssl import create_default_context, Purpose
from sanic.config import Config, LOGGING from sanic.config import Config
from sanic.constants import HTTP_METHODS from sanic.constants import HTTP_METHODS
from sanic.exceptions import ServerError, URLBuildError, SanicException from sanic.exceptions import ServerError, URLBuildError, SanicException
from sanic.handlers import ErrorHandler from sanic.handlers import ErrorHandler
from sanic.log import log from sanic.log import logger, error_logger, LOGGING_CONFIG_DEFAULTS
from sanic.response import HTTPResponse, StreamingHTTPResponse from sanic.response import HTTPResponse, StreamingHTTPResponse
from sanic.router import Router from sanic.router import Router
from sanic.server import serve, serve_multiple, HttpProtocol, Signal from sanic.server import serve, serve_multiple, HttpProtocol, Signal
@ -28,30 +28,21 @@ 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,
log_config=LOGGING, strict_slashes=False): strict_slashes=False, log_config=None):
if log_config:
logging.config.dictConfig(log_config)
# Only set up a default log handler if the
# end-user application didn't set anything up.
if not logging.root.handlers and log.level == logging.NOTSET:
formatter = logging.Formatter(
"%(asctime)s: %(levelname)s: %(message)s")
handler = logging.StreamHandler()
handler.setFormatter(formatter)
log.addHandler(handler)
log.setLevel(logging.INFO)
# Get name from previous stack frame # Get name from previous stack frame
if name is None: if name is None:
frame_records = stack()[1] frame_records = stack()[1]
name = getmodulename(frame_records[1]) name = getmodulename(frame_records[1])
# logging
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
self.name = name self.name = name
self.router = router or Router() self.router = router or Router()
self.request_class = request_class self.request_class = request_class
self.error_handler = error_handler or ErrorHandler() self.error_handler = error_handler or ErrorHandler()
self.config = Config(load_env=load_env) self.config = Config(load_env=load_env)
self.log_config = log_config
self.request_middleware = deque() self.request_middleware = deque()
self.response_middleware = deque() self.response_middleware = deque()
self.blueprints = {} self.blueprints = {}
@ -354,13 +345,13 @@ 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): stream_large_files=False, name='static', host=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) stream_large_files, name, host)
def blueprint(self, blueprint, **options): def blueprint(self, blueprint, **options):
"""Register a blueprint on the application. """Register a blueprint on the application.
@ -410,12 +401,32 @@ class Sanic:
URLBuildError URLBuildError
""" """
# find the route by the supplied view name # find the route by the supplied view name
uri, route = self.router.find_route_by_view_name(view_name) kw = {}
# special static files url_for
if view_name == 'static':
kw.update(name=kwargs.pop('name', 'static'))
elif view_name.endswith('.static'): # blueprint.static
kwargs.pop('name', None)
kw.update(name=view_name)
if not uri or not route: uri, route = self.router.find_route_by_view_name(view_name, **kw)
if not (uri and route):
raise URLBuildError('Endpoint with name `{}` was not found'.format( raise URLBuildError('Endpoint with name `{}` was not found'.format(
view_name)) view_name))
if view_name == 'static' or view_name.endswith('.static'):
filename = kwargs.pop('filename', None)
# it's static folder
if '<file_uri:' in uri:
folder_ = uri.split('<file_uri:', 1)[0]
if folder_.endswith('/'):
folder_ = folder_[:-1]
if filename.startswith('/'):
filename = filename[1:]
uri = '{}/{}'.format(folder_, filename)
if uri != '/' and uri.endswith('/'): if uri != '/' and uri.endswith('/'):
uri = uri[:-1] uri = uri[:-1]
@ -438,6 +449,16 @@ class Sanic:
if netloc is None and external: if netloc is None and external:
netloc = self.config.get('SERVER_NAME', '') netloc = self.config.get('SERVER_NAME', '')
if external:
if not scheme:
if ':' in netloc[:8]:
scheme = netloc[:8].split(':', 1)[0]
else:
scheme = 'http'
if '://' in netloc[:8]:
netloc = netloc.split('://', 1)[-1]
for match in matched_params: for match in matched_params:
name, _type, pattern = self.router.parse_parameter_string( name, _type, pattern = self.router.parse_parameter_string(
match) match)
@ -554,7 +575,7 @@ class Sanic:
response = await self._run_response_middleware(request, response = await self._run_response_middleware(request,
response) response)
except: except:
log.exception( error_logger.exception(
'Exception occured in one of response middleware handlers' 'Exception occured in one of response middleware handlers'
) )
@ -579,7 +600,7 @@ class Sanic:
def run(self, host=None, port=None, debug=False, ssl=None, def run(self, host=None, port=None, debug=False, ssl=None,
sock=None, workers=1, protocol=None, sock=None, workers=1, protocol=None,
backlog=100, stop_event=None, register_sys_signals=True, backlog=100, stop_event=None, register_sys_signals=True,
log_config=None): access_log=True):
"""Run the HTTP Server and listen until keyboard interrupt or term """Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing. signal. On termination, drain connections before closing.
@ -597,12 +618,10 @@ class Sanic:
:param protocol: Subclass of asyncio protocol class :param protocol: Subclass of asyncio protocol class
:return: Nothing :return: Nothing
""" """
if sock is None: if sock is None:
host, port = host or "127.0.0.1", port or 8000 host, port = host or "127.0.0.1", port or 8000
if log_config:
self.log_config = log_config
logging.config.dictConfig(log_config)
if protocol is None: if protocol is None:
protocol = (WebSocketProtocol if self.websocket_enabled protocol = (WebSocketProtocol if self.websocket_enabled
else HttpProtocol) else HttpProtocol)
@ -615,7 +634,7 @@ class Sanic:
host=host, port=port, debug=debug, ssl=ssl, sock=sock, host=host, port=port, debug=debug, ssl=ssl, sock=sock,
workers=workers, protocol=protocol, backlog=backlog, workers=workers, protocol=protocol, backlog=backlog,
register_sys_signals=register_sys_signals, register_sys_signals=register_sys_signals,
has_log=self.log_config is not None) access_log=access_log)
try: try:
self.is_running = True self.is_running = True
@ -624,12 +643,12 @@ class Sanic:
else: else:
serve_multiple(server_settings, workers) serve_multiple(server_settings, workers)
except: except:
log.exception( error_logger.exception(
'Experienced exception while trying to serve') 'Experienced exception while trying to serve')
raise raise
finally: finally:
self.is_running = False self.is_running = False
log.info("Server Stopped") logger.info("Server Stopped")
def stop(self): def stop(self):
"""This kills the Sanic""" """This kills the Sanic"""
@ -642,17 +661,16 @@ class Sanic:
async def create_server(self, host=None, port=None, debug=False, async def create_server(self, host=None, port=None, debug=False,
ssl=None, sock=None, protocol=None, ssl=None, sock=None, protocol=None,
backlog=100, stop_event=None, backlog=100, stop_event=None,
log_config=LOGGING): access_log=True):
"""Asynchronous version of `run`. """Asynchronous version of `run`.
NOTE: This does not support multiprocessing and is not the preferred NOTE: This does not support multiprocessing and is not the preferred
way to run a Sanic application. way to run a Sanic application.
""" """
if sock is None: if sock is None:
host, port = host or "127.0.0.1", port or 8000 host, port = host or "127.0.0.1", port or 8000
if log_config:
logging.config.dictConfig(log_config)
if protocol is None: if protocol is None:
protocol = (WebSocketProtocol if self.websocket_enabled protocol = (WebSocketProtocol if self.websocket_enabled
else HttpProtocol) else HttpProtocol)
@ -666,7 +684,7 @@ class Sanic:
host=host, port=port, debug=debug, ssl=ssl, sock=sock, host=host, port=port, debug=debug, ssl=ssl, sock=sock,
loop=get_event_loop(), protocol=protocol, loop=get_event_loop(), protocol=protocol,
backlog=backlog, run_async=True, backlog=backlog, run_async=True,
has_log=log_config is not None) access_log=access_log)
# Trigger before_start events # Trigger before_start events
await self.trigger_events( await self.trigger_events(
@ -711,7 +729,7 @@ class Sanic:
def _helper(self, host=None, port=None, debug=False, def _helper(self, host=None, port=None, debug=False,
ssl=None, sock=None, workers=1, loop=None, ssl=None, sock=None, workers=1, loop=None,
protocol=HttpProtocol, backlog=100, stop_event=None, protocol=HttpProtocol, backlog=100, stop_event=None,
register_sys_signals=True, run_async=False, has_log=True): register_sys_signals=True, run_async=False, access_log=True):
"""Helper function used by `run` and `create_server`.""" """Helper function used by `run` and `create_server`."""
if isinstance(ssl, dict): if isinstance(ssl, dict):
# try common aliaseses # try common aliaseses
@ -745,12 +763,14 @@ class Sanic:
'request_handler': self.handle_request, 'request_handler': self.handle_request,
'error_handler': self.error_handler, 'error_handler': self.error_handler,
'request_timeout': self.config.REQUEST_TIMEOUT, 'request_timeout': self.config.REQUEST_TIMEOUT,
'response_timeout': self.config.RESPONSE_TIMEOUT,
'keep_alive_timeout': self.config.KEEP_ALIVE_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE, 'request_max_size': self.config.REQUEST_MAX_SIZE,
'keep_alive': self.config.KEEP_ALIVE, 'keep_alive': self.config.KEEP_ALIVE,
'loop': loop, 'loop': loop,
'register_sys_signals': register_sys_signals, 'register_sys_signals': register_sys_signals,
'backlog': backlog, 'backlog': backlog,
'has_log': has_log, 'access_log': access_log,
'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE, 'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE,
'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE, 'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE,
'graceful_shutdown_timeout': self.config.GRACEFUL_SHUTDOWN_TIMEOUT 'graceful_shutdown_timeout': self.config.GRACEFUL_SHUTDOWN_TIMEOUT
@ -774,9 +794,9 @@ class Sanic:
server_settings[settings_name] = listeners server_settings[settings_name] = listeners
if debug: if debug:
log.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
if self.config.LOGO is not None: if self.config.LOGO is not None:
log.debug(self.config.LOGO) logger.debug(self.config.LOGO)
if run_async: if run_async:
server_settings['run_async'] = True server_settings['run_async'] = True
@ -786,6 +806,6 @@ class Sanic:
proto = "http" proto = "http"
if ssl is not None: if ssl is not None:
proto = "https" proto = "https"
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) logger.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
return server_settings return server_settings

View File

@ -218,6 +218,11 @@ class Blueprint:
:param uri: endpoint at which the route will be accessible. :param uri: endpoint at which the route will be accessible.
:param file_or_directory: Static asset. :param file_or_directory: Static asset.
""" """
name = kwargs.pop('name', 'static')
if not name.startswith(self.name + '.'):
name = '{}.{}'.format(self.name, name)
kwargs.update(name=name)
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

@ -1,103 +1,9 @@
import os import os
import sys
import syslog
import platform
import types import types
from sanic.log import DefaultFilter
SANIC_PREFIX = 'SANIC_' SANIC_PREFIX = 'SANIC_'
_address_dict = {
'Windows': ('localhost', 514),
'Darwin': '/var/run/syslog',
'Linux': '/dev/log',
'FreeBSD': '/var/run/log'
}
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'accessFilter': {
'()': DefaultFilter,
'param': [0, 10, 20]
},
'errorFilter': {
'()': DefaultFilter,
'param': [30, 40, 50]
}
},
'formatters': {
'simple': {
'format': '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S'
},
'access': {
'format': '%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: ' +
'%(request)s %(message)s %(status)d %(byte)d',
'datefmt': '%Y-%m-%d %H:%M:%S'
}
},
'handlers': {
'internal': {
'class': 'logging.StreamHandler',
'filters': ['accessFilter'],
'formatter': 'simple',
'stream': sys.stderr
},
'accessStream': {
'class': 'logging.StreamHandler',
'filters': ['accessFilter'],
'formatter': 'access',
'stream': sys.stderr
},
'errorStream': {
'class': 'logging.StreamHandler',
'filters': ['errorFilter'],
'formatter': 'simple',
'stream': sys.stderr
},
# before you use accessSysLog, be sure that log levels
# 0, 10, 20 have been enabled in you syslog configuration
# otherwise you won't be able to see the output in syslog
# logging file.
'accessSysLog': {
'class': 'logging.handlers.SysLogHandler',
'address': _address_dict.get(platform.system(),
('localhost', 514)),
'facility': syslog.LOG_DAEMON,
'filters': ['accessFilter'],
'formatter': 'access'
},
'errorSysLog': {
'class': 'logging.handlers.SysLogHandler',
'address': _address_dict.get(platform.system(),
('localhost', 514)),
'facility': syslog.LOG_DAEMON,
'filters': ['errorFilter'],
'formatter': 'simple'
},
},
'loggers': {
'sanic': {
'level': 'INFO',
'handlers': ['internal', 'errorStream']
},
'network': {
'level': 'INFO',
'handlers': ['accessStream', 'errorStream']
}
}
}
# this happens when using container or systems without syslog
# keep things in config would cause file not exists error
_addr = LOGGING['handlers']['accessSysLog']['address']
if type(_addr) is str and not os.path.exists(_addr):
LOGGING['handlers'].pop('accessSysLog')
LOGGING['handlers'].pop('errorSysLog')
class Config(dict): class Config(dict):
def __init__(self, defaults=None, load_env=True, keep_alive=True): def __init__(self, defaults=None, load_env=True, keep_alive=True):
@ -125,7 +31,15 @@ class Config(dict):
""" """
self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes
self.REQUEST_TIMEOUT = 60 # 60 seconds self.REQUEST_TIMEOUT = 60 # 60 seconds
self.RESPONSE_TIMEOUT = 60 # 60 seconds
self.KEEP_ALIVE = keep_alive self.KEEP_ALIVE = keep_alive
# Apache httpd server default keepalive timeout = 5 seconds
# Nginx server default keepalive timeout = 75 seconds
# Nginx performance tuning guidelines uses keepalive = 15 seconds
# IE client hard keepalive limit = 60 seconds
# Firefox client hard keepalive limit = 115 seconds
self.KEEP_ALIVE_TIMEOUT = 5 # 5 seconds
self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes
self.WEBSOCKET_MAX_QUEUE = 32 self.WEBSOCKET_MAX_QUEUE = 32
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec

View File

@ -98,6 +98,7 @@ class Cookie(dict):
def __setitem__(self, key, value): def __setitem__(self, key, value):
if key not in self._keys: if key not in self._keys:
raise KeyError("Unknown cookie property") raise KeyError("Unknown cookie property")
if value is not False:
return super().__setitem__(key, value) return super().__setitem__(key, value)
def encode(self, encoding): def encode(self, encoding):

View File

@ -155,6 +155,13 @@ class ServerError(SanicException):
pass pass
@add_status_code(503)
class ServiceUnavailable(SanicException):
"""The server is currently unavailable (because it is overloaded or
down for maintenance). Generally, this is a temporary state."""
pass
class URLBuildError(ServerError): class URLBuildError(ServerError):
pass pass
@ -170,6 +177,13 @@ class FileNotFound(NotFound):
@add_status_code(408) @add_status_code(408)
class RequestTimeout(SanicException): class RequestTimeout(SanicException):
"""The Web server (running the Web site) thinks that there has been too
long an interval of time between 1) the establishment of an IP
connection (socket) between the client and the server and
2) the receipt of any data on that socket, so the server has dropped
the connection. The socket connection has actually been lost - the Web
server has 'timed out' on that particular socket connection.
"""
pass pass

View File

@ -12,7 +12,7 @@ from sanic.exceptions import (
TRACEBACK_WRAPPER_HTML, TRACEBACK_WRAPPER_HTML,
TRACEBACK_WRAPPER_INNER_HTML, TRACEBACK_WRAPPER_INNER_HTML,
TRACEBACK_BORDER) TRACEBACK_BORDER)
from sanic.log import log from sanic.log import logger
from sanic.response import text, html from sanic.response import text, html
@ -86,12 +86,13 @@ class ErrorHandler:
self.log(format_exc()) self.log(format_exc())
if self.debug: if self.debug:
url = getattr(request, 'url', 'unknown') url = getattr(request, 'url', 'unknown')
response_message = ( response_message = ('Exception raised in exception handler '
'Exception raised in exception handler "{}" ' '"%s" for uri: "%s"\n%s')
'for uri: "{}"\n{}').format( logger.error(response_message,
handler.__name__, url, format_exc()) handler.__name__, url, format_exc())
log.error(response_message)
return text(response_message, 500) return text(response_message % (
handler.__name__, url, format_exc()), 500)
else: else:
return text('An error occurred while handling an error', 500) return text('An error occurred while handling an error', 500)
return response return response
@ -101,7 +102,7 @@ class ErrorHandler:
Override this method in an ErrorHandler subclass to prevent Override this method in an ErrorHandler subclass to prevent
logging exceptions. logging exceptions.
""" """
getattr(log, level)(message) getattr(logger, level)(message)
def default(self, request, exception): def default(self, request, exception):
self.log(format_exc()) self.log(format_exc())
@ -114,10 +115,9 @@ class ErrorHandler:
elif self.debug: elif self.debug:
html_output = self._render_traceback_html(exception, request) html_output = self._render_traceback_html(exception, request)
response_message = ( response_message = ('Exception occurred while handling uri: '
'Exception occurred while handling uri: "{}"\n{}'.format( '"%s"\n%s')
request.url, format_exc())) logger.error(response_message, request.url, format_exc())
log.error(response_message)
return html(html_output, status=500) return html(html_output, status=500)
else: else:
return html(INTERNAL_SERVER_ERROR_HTML, status=500) return html(INTERNAL_SERVER_ERROR_HTML, status=500)

View File

@ -1,18 +1,63 @@
import logging import logging
import sys
class DefaultFilter(logging.Filter): LOGGING_CONFIG_DEFAULTS = dict(
version=1,
disable_existing_loggers=False,
def __init__(self, param=None): loggers={
self.param = param "root": {
"level": "INFO",
"handlers": ["console"]
},
"sanic.error": {
"level": "INFO",
"handlers": ["error_console"],
"propagate": True,
"qualname": "sanic.error"
},
def filter(self, record): "sanic.access": {
if self.param is None: "level": "INFO",
return True "handlers": ["access_console"],
if record.levelno in self.param: "propagate": True,
return True "qualname": "sanic.access"
return False }
},
handlers={
"console": {
"class": "logging.StreamHandler",
"formatter": "generic",
"stream": sys.stdout
},
"error_console": {
"class": "logging.StreamHandler",
"formatter": "generic",
"stream": sys.stderr
},
"access_console": {
"class": "logging.StreamHandler",
"formatter": "access",
"stream": sys.stdout
},
},
formatters={
"generic": {
"format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s",
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
"class": "logging.Formatter"
},
"access": {
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: " +
"%(request)s %(message)s %(status)d %(byte)d",
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
"class": "logging.Formatter"
},
}
)
log = logging.getLogger('sanic') logger = logging.getLogger('root')
netlog = logging.getLogger('network') error_logger = logging.getLogger('sanic.error')
access_logger = logging.getLogger('sanic.access')

View File

@ -17,10 +17,11 @@ except ImportError:
json_loads = json.loads json_loads = json.loads
from sanic.exceptions import InvalidUsage from sanic.exceptions import InvalidUsage
from sanic.log import log from sanic.log import error_logger
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1 # HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
# > If the media type remains unknown, the recipient SHOULD treat it # > If the media type remains unknown, the recipient SHOULD treat it
# > as type "application/octet-stream" # > as type "application/octet-stream"
@ -68,11 +69,23 @@ class Request(dict):
self._cookies = None self._cookies = None
self.stream = None self.stream = None
def __repr__(self):
if self.method is None or not self.path:
return '<{0}>'.format(self.__class__.__name__)
return '<{0}: {1} {2}>'.format(self.__class__.__name__,
self.method,
self.path)
@property @property
def json(self): def json(self):
if self.parsed_json is None: if self.parsed_json is None:
self.load_json()
return self.parsed_json
def load_json(self, loads=json_loads):
try: try:
self.parsed_json = json_loads(self.body) self.parsed_json = loads(self.body)
except Exception: except Exception:
if not self.body: if not self.body:
return None return None
@ -114,7 +127,7 @@ class Request(dict):
self.parsed_form, self.parsed_files = ( self.parsed_form, self.parsed_files = (
parse_multipart_form(self.body, boundary)) parse_multipart_form(self.body, boundary))
except Exception: except Exception:
log.exception("Failed when parsing form") error_logger.exception("Failed when parsing form")
return self.parsed_form return self.parsed_form

View File

@ -56,6 +56,7 @@ ALL_STATUS_CODES = {
415: b'Unsupported Media Type', 415: b'Unsupported Media Type',
416: b'Requested Range Not Satisfiable', 416: b'Requested Range Not Satisfiable',
417: b'Expectation Failed', 417: b'Expectation Failed',
418: b'I\'m a teapot',
422: b'Unprocessable Entity', 422: b'Unprocessable Entity',
423: b'Locked', 423: b'Locked',
424: b'Failed Dependency', 424: b'Failed Dependency',
@ -63,6 +64,7 @@ ALL_STATUS_CODES = {
428: b'Precondition Required', 428: b'Precondition Required',
429: b'Too Many Requests', 429: b'Too Many Requests',
431: b'Request Header Fields Too Large', 431: b'Request Header Fields Too Large',
451: b'Unavailable For Legal Reasons',
500: b'Internal Server Error', 500: b'Internal Server Error',
501: b'Not Implemented', 501: b'Not Implemented',
502: b'Bad Gateway', 502: b'Bad Gateway',
@ -235,7 +237,8 @@ class HTTPResponse(BaseHTTPResponse):
def json(body, status=200, headers=None, def json(body, status=200, headers=None,
content_type="application/json", **kwargs): content_type="application/json", dumps=json_dumps,
**kwargs):
""" """
Returns response object with body in json format. Returns response object with body in json format.
@ -244,7 +247,7 @@ def json(body, status=200, headers=None,
:param headers: Custom Headers. :param headers: Custom Headers.
:param kwargs: Remaining arguments that are passed to the json encoder. :param kwargs: Remaining arguments that are passed to the json encoder.
""" """
return HTTPResponse(json_dumps(body, **kwargs), headers=headers, return HTTPResponse(dumps(body, **kwargs), headers=headers,
status=status, content_type=content_type) status=status, content_type=content_type)

View File

@ -68,6 +68,7 @@ class Router:
def __init__(self): def __init__(self):
self.routes_all = {} self.routes_all = {}
self.routes_names = {} self.routes_names = {}
self.routes_static_files = {}
self.routes_static = {} self.routes_static = {}
self.routes_dynamic = defaultdict(list) self.routes_dynamic = defaultdict(list)
self.routes_always_check = [] self.routes_always_check = []
@ -148,6 +149,7 @@ class Router:
provided, any method is allowed provided, any method is allowed
:param handler: request handler function. :param handler: request handler function.
When executed, it should provide a response object. When executed, it should provide a response object.
:param name: user defined route name for url_for
:return: Nothing :return: Nothing
""" """
if host is not None: if host is not None:
@ -231,6 +233,12 @@ class Router:
# prefix the handler name with the blueprint name # prefix the handler name with the blueprint name
# if available # if available
# special prefix for static files
is_static = False
if name and name.startswith('_static_'):
is_static = True
name = name.split('_static_', 1)[-1]
if hasattr(handler, '__blueprintname__'): if hasattr(handler, '__blueprintname__'):
handler_name = '{}.{}'.format( handler_name = '{}.{}'.format(
handler.__blueprintname__, name or handler.__name__) handler.__blueprintname__, name or handler.__name__)
@ -245,8 +253,14 @@ class Router:
parameters=parameters, name=handler_name, uri=uri) parameters=parameters, name=handler_name, uri=uri)
self.routes_all[uri] = route self.routes_all[uri] = route
pairs = self.routes_names.get(handler_name) if is_static:
if not (pairs and (pairs[0] + '/' == uri or uri + '/' == pairs[0])): pair = self.routes_static_files.get(handler_name)
if not (pair and (pair[0] + '/' == uri or uri + '/' == pair[0])):
self.routes_static_files[handler_name] = (uri, route)
else:
pair = self.routes_names.get(handler_name)
if not (pair and (pair[0] + '/' == uri or uri + '/' == pair[0])):
self.routes_names[handler_name] = (uri, route) self.routes_names[handler_name] = (uri, route)
if properties['unhashable']: if properties['unhashable']:
@ -274,6 +288,11 @@ class Router:
self.routes_names.pop(handler_name) self.routes_names.pop(handler_name)
break break
for handler_name, pairs in self.routes_static_files.items():
if pairs[0] == uri:
self.routes_static_files.pop(handler_name)
break
except KeyError: except KeyError:
raise RouteDoesNotExist("Route was not registered: {}".format(uri)) raise RouteDoesNotExist("Route was not registered: {}".format(uri))
@ -289,15 +308,19 @@ class Router:
self._get.cache_clear() self._get.cache_clear()
@lru_cache(maxsize=ROUTER_CACHE_SIZE) @lru_cache(maxsize=ROUTER_CACHE_SIZE)
def find_route_by_view_name(self, view_name): def find_route_by_view_name(self, view_name, name=None):
"""Find a route in the router based on the specified view name. """Find a route in the router based on the specified view name.
:param view_name: string of view name to search by :param view_name: string of view name to search by
:param kwargs: additional params, usually for static files
:return: tuple containing (uri, Route) :return: tuple containing (uri, Route)
""" """
if not view_name: if not view_name:
return (None, None) return (None, None)
if view_name == 'static' or view_name.endswith('.static'):
return self.routes_static_files.get(name, (None, None))
return self.routes_names.get(view_name, (None, None)) return self.routes_names.get(view_name, (None, None))
def get(self, request): def get(self, request):

View File

@ -24,11 +24,12 @@ try:
except ImportError: except ImportError:
async_loop = asyncio async_loop = asyncio
from sanic.log import log, netlog from sanic.log import logger, access_logger
from sanic.response import HTTPResponse from sanic.response import HTTPResponse
from sanic.request import Request from sanic.request import Request
from sanic.exceptions import ( from sanic.exceptions import (
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError) RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError,
ServiceUnavailable)
current_time = None current_time = None
@ -63,17 +64,20 @@ class HttpProtocol(asyncio.Protocol):
# request params # request params
'parser', 'request', 'url', 'headers', 'parser', 'request', 'url', 'headers',
# request config # request config
'request_handler', 'request_timeout', 'request_max_size', 'request_handler', 'request_timeout', 'response_timeout',
'request_class', 'is_request_stream', 'router', 'keep_alive_timeout', 'request_max_size', 'request_class',
# enable or disable access log / error log purpose 'is_request_stream', 'router',
'has_log', # enable or disable access log purpose
'access_log',
# connection management # connection management
'_total_request_size', '_timeout_handler', '_last_communication_time', '_total_request_size', '_request_timeout_handler',
'_is_stream_handler') '_response_timeout_handler', '_keep_alive_timeout_handler',
'_last_request_time', '_last_response_time', '_is_stream_handler')
def __init__(self, *, loop, request_handler, error_handler, def __init__(self, *, loop, request_handler, error_handler,
signal=Signal(), connections=set(), request_timeout=60, signal=Signal(), connections=set(), request_timeout=60,
request_max_size=None, request_class=None, has_log=True, response_timeout=60, keep_alive_timeout=15,
request_max_size=None, request_class=None, access_log=True,
keep_alive=True, is_request_stream=False, router=None, keep_alive=True, is_request_stream=False, router=None,
state=None, debug=False, **kwargs): state=None, debug=False, **kwargs):
self.loop = loop self.loop = loop
@ -84,18 +88,23 @@ class HttpProtocol(asyncio.Protocol):
self.headers = None self.headers = None
self.router = router self.router = router
self.signal = signal self.signal = signal
self.has_log = has_log self.access_log = access_log
self.connections = connections self.connections = connections
self.request_handler = request_handler self.request_handler = request_handler
self.error_handler = error_handler self.error_handler = error_handler
self.request_timeout = request_timeout self.request_timeout = request_timeout
self.response_timeout = response_timeout
self.keep_alive_timeout = keep_alive_timeout
self.request_max_size = request_max_size self.request_max_size = request_max_size
self.request_class = request_class or Request self.request_class = request_class or Request
self.is_request_stream = is_request_stream self.is_request_stream = is_request_stream
self._is_stream_handler = False self._is_stream_handler = False
self._total_request_size = 0 self._total_request_size = 0
self._timeout_handler = None self._request_timeout_handler = None
self._response_timeout_handler = None
self._keep_alive_timeout_handler = None
self._last_request_time = None self._last_request_time = None
self._last_response_time = None
self._request_handler_task = None self._request_handler_task = None
self._request_stream_task = None self._request_stream_task = None
self._keep_alive = keep_alive self._keep_alive = keep_alive
@ -118,22 +127,32 @@ class HttpProtocol(asyncio.Protocol):
def connection_made(self, transport): def connection_made(self, transport):
self.connections.add(self) self.connections.add(self)
self._timeout_handler = self.loop.call_later( self._request_timeout_handler = self.loop.call_later(
self.request_timeout, self.connection_timeout) self.request_timeout, self.request_timeout_callback)
self.transport = transport self.transport = transport
self._last_request_time = current_time self._last_request_time = current_time
def connection_lost(self, exc): def connection_lost(self, exc):
self.connections.discard(self) self.connections.discard(self)
self._timeout_handler.cancel() if self._request_timeout_handler:
self._request_timeout_handler.cancel()
if self._response_timeout_handler:
self._response_timeout_handler.cancel()
if self._keep_alive_timeout_handler:
self._keep_alive_timeout_handler.cancel()
def connection_timeout(self): def request_timeout_callback(self):
# Check if # See the docstring in the RequestTimeout exception, to see
# exactly what this timeout is checking for.
# Check if elapsed time since request initiated exceeds our
# configured maximum request timeout value
time_elapsed = current_time - self._last_request_time time_elapsed = current_time - self._last_request_time
if time_elapsed < self.request_timeout: if time_elapsed < self.request_timeout:
time_left = self.request_timeout - time_elapsed time_left = self.request_timeout - time_elapsed
self._timeout_handler = ( self._request_timeout_handler = (
self.loop.call_later(time_left, self.connection_timeout)) self.loop.call_later(time_left,
self.request_timeout_callback)
)
else: else:
if self._request_stream_task: if self._request_stream_task:
self._request_stream_task.cancel() self._request_stream_task.cancel()
@ -144,6 +163,36 @@ class HttpProtocol(asyncio.Protocol):
except RequestTimeout as exception: except RequestTimeout as exception:
self.write_error(exception) self.write_error(exception)
def response_timeout_callback(self):
# Check if elapsed time since response was initiated exceeds our
# configured maximum request timeout value
time_elapsed = current_time - self._last_request_time
if time_elapsed < self.response_timeout:
time_left = self.response_timeout - time_elapsed
self._response_timeout_handler = (
self.loop.call_later(time_left,
self.response_timeout_callback)
)
else:
try:
raise ServiceUnavailable('Response Timeout')
except ServiceUnavailable as exception:
self.write_error(exception)
def keep_alive_timeout_callback(self):
# Check if elapsed time since last response exceeds our configured
# maximum keep alive timeout value
time_elapsed = current_time - self._last_response_time
if time_elapsed < self.keep_alive_timeout:
time_left = self.keep_alive_timeout - time_elapsed
self._keep_alive_timeout_handler = (
self.loop.call_later(time_left,
self.keep_alive_timeout_callback)
)
else:
logger.info('KeepAlive Timeout. Closing connection.')
self.transport.close()
# -------------------------------------------- # # -------------------------------------------- #
# Parsing # Parsing
# -------------------------------------------- # # -------------------------------------------- #
@ -189,10 +238,12 @@ class HttpProtocol(asyncio.Protocol):
and int(value) > self.request_max_size: and int(value) > self.request_max_size:
exception = PayloadTooLarge('Payload Too Large') exception = PayloadTooLarge('Payload Too Large')
self.write_error(exception) self.write_error(exception)
try:
value = value.decode()
except UnicodeDecodeError:
value = value.decode('latin_1')
self.headers.append( self.headers.append(
(self._header_fragment.decode().casefold(), (self._header_fragment.decode().casefold(), value))
value.decode()))
self._header_fragment = b'' self._header_fragment = b''
@ -204,6 +255,11 @@ class HttpProtocol(asyncio.Protocol):
method=self.parser.get_method().decode(), method=self.parser.get_method().decode(),
transport=self.transport transport=self.transport
) )
# Remove any existing KeepAlive handler here,
# It will be recreated if required on the new request.
if self._keep_alive_timeout_handler:
self._keep_alive_timeout_handler.cancel()
self._keep_alive_timeout_handler = None
if self.is_request_stream: if self.is_request_stream:
self._is_stream_handler = self.router.is_stream_handler( self._is_stream_handler = self.router.is_stream_handler(
self.request) self.request)
@ -219,6 +275,11 @@ class HttpProtocol(asyncio.Protocol):
self.request.body.append(body) self.request.body.append(body)
def on_message_complete(self): def on_message_complete(self):
# Entire request (headers and whole body) is received.
# We can cancel and remove the request timeout handler now.
if self._request_timeout_handler:
self._request_timeout_handler.cancel()
self._request_timeout_handler = None
if self.is_request_stream and self._is_stream_handler: if self.is_request_stream and self._is_stream_handler:
self._request_stream_task = self.loop.create_task( self._request_stream_task = self.loop.create_task(
self.request.stream.put(None)) self.request.stream.put(None))
@ -227,6 +288,9 @@ class HttpProtocol(asyncio.Protocol):
self.execute_request_handler() self.execute_request_handler()
def execute_request_handler(self): def execute_request_handler(self):
self._response_timeout_handler = self.loop.call_later(
self.response_timeout, self.response_timeout_callback)
self._last_request_time = current_time
self._request_handler_task = self.loop.create_task( self._request_handler_task = self.loop.create_task(
self.request_handler( self.request_handler(
self.request, self.request,
@ -236,35 +300,50 @@ class HttpProtocol(asyncio.Protocol):
# -------------------------------------------- # # -------------------------------------------- #
# Responding # Responding
# -------------------------------------------- # # -------------------------------------------- #
def log_response(self, response):
if self.access_log:
extra = {
'status': getattr(response, 'status', 0),
}
if isinstance(response, HTTPResponse):
extra['byte'] = len(response.body)
else:
extra['byte'] = -1
if self.request:
extra['host'] = '{0}:{1}'.format(self.request.ip[0],
self.request.ip[1])
extra['request'] = '{0} {1}'.format(self.request.method,
self.request.url)
else:
extra['host'] = 'UNKNOWN'
extra['request'] = 'nil'
access_logger.info('', extra=extra)
def write_response(self, response): def write_response(self, response):
""" """
Writes response content synchronously to the transport. Writes response content synchronously to the transport.
""" """
if self._response_timeout_handler:
self._response_timeout_handler.cancel()
self._response_timeout_handler = None
try: try:
keep_alive = self.keep_alive keep_alive = self.keep_alive
self.transport.write( self.transport.write(
response.output( response.output(
self.request.version, keep_alive, self.request.version, keep_alive,
self.request_timeout)) self.keep_alive_timeout))
if self.has_log: self.log_response(response)
netlog.info('', extra={
'status': response.status,
'byte': len(response.body),
'host': '{0}:{1}'.format(self.request.ip[0],
self.request.ip[1]),
'request': '{0} {1}'.format(self.request.method,
self.request.url)
})
except AttributeError: except AttributeError:
log.error( logger.error('Invalid response object for url %s, '
('Invalid response object for url {}, ' 'Expected Type: HTTPResponse, Actual Type: %s',
'Expected Type: HTTPResponse, Actual Type: {}').format( self.url, type(response))
self.url, type(response)))
self.write_error(ServerError('Invalid response type')) self.write_error(ServerError('Invalid response type'))
except RuntimeError: except RuntimeError:
log.error( logger.error('Connection lost before response written @ %s',
'Connection lost before response written @ {}'.format( self.request.ip)
self.request.ip))
except Exception as e: except Exception as e:
self.bail_out( self.bail_out(
"Writing response failed, connection closed {}".format( "Writing response failed, connection closed {}".format(
@ -273,7 +352,10 @@ class HttpProtocol(asyncio.Protocol):
if not keep_alive: if not keep_alive:
self.transport.close() self.transport.close()
else: else:
self._last_request_time = current_time self._keep_alive_timeout_handler = self.loop.call_later(
self.keep_alive_timeout,
self.keep_alive_timeout_callback)
self._last_response_time = current_time
self.cleanup() self.cleanup()
async def stream_response(self, response): async def stream_response(self, response):
@ -282,31 +364,23 @@ class HttpProtocol(asyncio.Protocol):
the transport to the response so the response consumer can the transport to the response so the response consumer can
write to the response as needed. write to the response as needed.
""" """
if self._response_timeout_handler:
self._response_timeout_handler.cancel()
self._response_timeout_handler = None
try: try:
keep_alive = self.keep_alive keep_alive = self.keep_alive
response.transport = self.transport response.transport = self.transport
await response.stream( await response.stream(
self.request.version, keep_alive, self.request_timeout) self.request.version, keep_alive, self.keep_alive_timeout)
if self.has_log: self.log_response(response)
netlog.info('', extra={
'status': response.status,
'byte': -1,
'host': '{0}:{1}'.format(self.request.ip[0],
self.request.ip[1]),
'request': '{0} {1}'.format(self.request.method,
self.request.url)
})
except AttributeError: except AttributeError:
log.error( logger.error('Invalid response object for url %s, '
('Invalid response object for url {}, ' 'Expected Type: HTTPResponse, Actual Type: %s',
'Expected Type: HTTPResponse, Actual Type: {}').format( self.url, type(response))
self.url, type(response)))
self.write_error(ServerError('Invalid response type')) self.write_error(ServerError('Invalid response type'))
except RuntimeError: except RuntimeError:
log.error( logger.error('Connection lost before response written @ %s',
'Connection lost before response written @ {}'.format( self.request.ip)
self.request.ip))
except Exception as e: except Exception as e:
self.bail_out( self.bail_out(
"Writing response failed, connection closed {}".format( "Writing response failed, connection closed {}".format(
@ -315,58 +389,52 @@ class HttpProtocol(asyncio.Protocol):
if not keep_alive: if not keep_alive:
self.transport.close() self.transport.close()
else: else:
self._last_request_time = current_time self._keep_alive_timeout_handler = self.loop.call_later(
self.keep_alive_timeout,
self.keep_alive_timeout_callback)
self._last_response_time = current_time
self.cleanup() self.cleanup()
def write_error(self, exception): def write_error(self, exception):
# An error _is_ a response.
# Don't throw a response timeout, when a response _is_ given.
if self._response_timeout_handler:
self._response_timeout_handler.cancel()
self._response_timeout_handler = None
response = None response = None
try: try:
response = self.error_handler.response(self.request, exception) response = self.error_handler.response(self.request, exception)
version = self.request.version if self.request else '1.1' version = self.request.version if self.request else '1.1'
self.transport.write(response.output(version)) self.transport.write(response.output(version))
except RuntimeError: except RuntimeError:
log.error( logger.error('Connection lost before error written @ %s',
'Connection lost before error written @ {}'.format( self.request.ip if self.request else 'Unknown')
self.request.ip if self.request else 'Unknown'))
except Exception as e: except Exception as e:
self.bail_out( self.bail_out(
"Writing error failed, connection closed {}".format(repr(e)), "Writing error failed, connection closed {}".format(
from_error=True) repr(e)), from_error=True
)
finally: finally:
if self.has_log: if self.parser and (self.keep_alive
extra = dict() or getattr(response, 'status', 0) == 408):
if isinstance(response, HTTPResponse): self.log_response(response)
extra['status'] = response.status
extra['byte'] = len(response.body)
else:
extra['status'] = 0
extra['byte'] = -1
if self.request:
extra['host'] = '%s:%d' % self.request.ip,
extra['request'] = '%s %s' % (self.request.method,
self.url)
else:
extra['host'] = 'UNKNOWN'
extra['request'] = 'nil'
if self.parser and not (self.keep_alive
and extra['status'] == 408):
netlog.info('', extra=extra)
self.transport.close() self.transport.close()
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():
log.error( logger.error("Transport closed @ %s and exception "
("Transport closed @ {} and exception " "experienced during error handling",
"experienced during error handling").format( self.transport.get_extra_info('peername'))
self.transport.get_extra_info('peername'))) logger.debug('Exception:\n%s', traceback.format_exc())
log.debug(
'Exception:\n{}'.format(traceback.format_exc()))
else: else:
exception = ServerError(message) exception = ServerError(message)
self.write_error(exception) self.write_error(exception)
log.error(message) logger.error(message)
def cleanup(self): def cleanup(self):
"""This is called when KeepAlive feature is used,
it resets the connection in order for it to be able
to handle receiving another request on the same connection."""
self.parser = None self.parser = None
self.request = None self.request = None
self.url = None self.url = None
@ -421,12 +489,13 @@ def trigger_events(events, loop):
def serve(host, port, request_handler, error_handler, before_start=None, def serve(host, port, request_handler, error_handler, before_start=None,
after_start=None, before_stop=None, after_stop=None, debug=False, after_start=None, before_stop=None, after_stop=None, debug=False,
request_timeout=60, ssl=None, sock=None, request_max_size=None, request_timeout=60, response_timeout=60, keep_alive_timeout=60,
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, ssl=None, sock=None, request_max_size=None, reuse_port=False,
loop=None, protocol=HttpProtocol, backlog=100,
register_sys_signals=True, run_async=False, connections=None, register_sys_signals=True, run_async=False, connections=None,
signal=Signal(), request_class=None, has_log=True, keep_alive=True, signal=Signal(), request_class=None, access_log=True,
is_request_stream=False, router=None, websocket_max_size=None, keep_alive=True, is_request_stream=False, router=None,
websocket_max_queue=None, state=None, websocket_max_size=None, websocket_max_queue=None, state=None,
graceful_shutdown_timeout=15.0): graceful_shutdown_timeout=15.0):
"""Start asynchronous HTTP Server on an individual process. """Start asynchronous HTTP Server on an individual process.
@ -453,7 +522,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
:param loop: asyncio compatible event loop :param loop: asyncio compatible event loop
:param protocol: subclass of asyncio protocol class :param protocol: subclass of asyncio protocol class
:param request_class: Request class to use :param request_class: Request class to use
:param has_log: disable/enable access log and error log :param access_log: disable/enable access log
:param is_request_stream: disable/enable Request.stream :param is_request_stream: disable/enable Request.stream
:param router: Router object :param router: Router object
:return: Nothing :return: Nothing
@ -474,9 +543,11 @@ def serve(host, port, request_handler, error_handler, before_start=None,
request_handler=request_handler, request_handler=request_handler,
error_handler=error_handler, error_handler=error_handler,
request_timeout=request_timeout, request_timeout=request_timeout,
response_timeout=response_timeout,
keep_alive_timeout=keep_alive_timeout,
request_max_size=request_max_size, request_max_size=request_max_size,
request_class=request_class, request_class=request_class,
has_log=has_log, access_log=access_log,
keep_alive=keep_alive, keep_alive=keep_alive,
is_request_stream=is_request_stream, is_request_stream=is_request_stream,
router=router, router=router,
@ -508,7 +579,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:
log.exception("Unable to start server") logger.exception("Unable to start server")
return return
trigger_events(after_start, loop) trigger_events(after_start, loop)
@ -519,14 +590,14 @@ def serve(host, port, request_handler, error_handler, before_start=None,
try: try:
loop.add_signal_handler(_signal, loop.stop) loop.add_signal_handler(_signal, loop.stop)
except NotImplementedError: except NotImplementedError:
log.warn('Sanic tried to use loop.add_signal_handler but it is' logger.warning('Sanic tried to use loop.add_signal_handler '
' not implemented on this platform.') 'but it is not implemented on this platform.')
pid = os.getpid() pid = os.getpid()
try: try:
log.info('Starting worker [{}]'.format(pid)) logger.info('Starting worker [%s]', pid)
loop.run_forever() loop.run_forever()
finally: finally:
log.info("Stopping worker [{}]".format(pid)) logger.info("Stopping worker [%s]", pid)
# Run the on_stop function if provided # Run the on_stop function if provided
trigger_events(before_stop, loop) trigger_events(before_stop, loop)
@ -588,8 +659,7 @@ def serve_multiple(server_settings, workers):
server_settings['port'] = None server_settings['port'] = None
def sig_handler(signal, frame): def sig_handler(signal, frame):
log.info("Received signal {}. Shutting down.".format( logger.info("Received signal %s. Shutting down.", Signals(signal).name)
Signals(signal).name))
for process in processes: for process in processes:
os.kill(process.pid, SIGINT) os.kill(process.pid, SIGINT)

View File

@ -18,7 +18,7 @@ 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): stream_large_files, name='static', host=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
@ -39,6 +39,7 @@ def register(app, uri, file_or_directory, pattern,
than the file() handler to send the file than the file() handler to send the file
If this is an integer, this represents the If this is an integer, this represents the
threshold size to switch to file_stream() threshold size to switch to file_stream()
:param name: user defined name used for url_for
""" """
# If we're not trying to match a file directly, # If we're not trying to match a file directly,
# serve from the folder # serve from the folder
@ -117,4 +118,8 @@ def register(app, uri, file_or_directory, pattern,
path=file_or_directory, path=file_or_directory,
relative_url=file_uri) relative_url=file_uri)
app.route(uri, methods=['GET', 'HEAD'])(_handler) # special prefix for static files
if not name.startswith('_static_'):
name = '_static_{}'.format(name)
app.route(uri, methods=['GET', 'HEAD'], name=name, host=host)(_handler)

View File

@ -1,7 +1,7 @@
import traceback import traceback
from json import JSONDecodeError from json import JSONDecodeError
from sanic.log import log from sanic.log import logger
HOST = '127.0.0.1' HOST = '127.0.0.1'
PORT = 42101 PORT = 42101
@ -19,7 +19,7 @@ class SanicTestClient:
url = 'http://{host}:{port}{uri}'.format( url = 'http://{host}:{port}{uri}'.format(
host=HOST, port=PORT, uri=uri) host=HOST, port=PORT, uri=uri)
log.info(url) logger.info(url)
conn = aiohttp.TCPConnector(verify_ssl=False) conn = aiohttp.TCPConnector(verify_ssl=False)
async with aiohttp.ClientSession( async with aiohttp.ClientSession(
cookies=cookies, connector=conn) as session: cookies=cookies, connector=conn) as session:
@ -61,7 +61,7 @@ class SanicTestClient:
**request_kwargs) **request_kwargs)
results[-1] = response results[-1] = response
except Exception as e: except Exception as e:
log.error( logger.error(
'Exception:\n{}'.format(traceback.format_exc())) 'Exception:\n{}'.format(traceback.format_exc()))
exceptions.append(e) exceptions.append(e)
self.app.stop() self.app.stop()

View File

@ -142,9 +142,8 @@ class GunicornWorker(base.Worker):
) )
if self.max_requests and req_count > self.max_requests: if self.max_requests and req_count > self.max_requests:
self.alive = False self.alive = False
self.log.info( self.log.info("Max requests exceeded, shutting down: %s",
"Max requests exceeded, shutting down: %s", self self)
)
elif pid == os.getpid() and self.ppid != os.getppid(): elif pid == os.getpid() and self.ppid != os.getppid():
self.alive = False self.alive = False
self.log.info("Parent changed, shutting down: %s", self) self.log.info("Parent changed, shutting down: %s", self)

View File

@ -0,0 +1 @@
I am just a regular static file that needs to have its uri decoded

BIN
tests/static/bp/python.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1 @@
I am just a regular static file

View File

@ -25,6 +25,25 @@ def test_cookies():
assert response.text == 'Cookies are: working!' assert response.text == 'Cookies are: working!'
assert response_cookies['right_back'].value == 'at you' assert response_cookies['right_back'].value == 'at you'
@pytest.mark.parametrize("httponly,expected", [
(False, False),
(True, True),
])
def test_false_cookies_encoded(httponly, expected):
app = Sanic('test_text')
@app.route('/')
def handler(request):
response = text('hello cookies')
response.cookies['hello'] = 'world'
response.cookies['hello']['httponly'] = httponly
return text(response.cookies['hello'].encode('utf8'))
request, response = app.test_client.get('/')
assert ('HttpOnly' in response.text) == expected
@pytest.mark.parametrize("httponly,expected", [ @pytest.mark.parametrize("httponly,expected", [
(False, False), (False, False),
(True, True), (True, True),
@ -34,7 +53,7 @@ def test_false_cookies(httponly, expected):
@app.route('/') @app.route('/')
def handler(request): def handler(request):
response = text('Cookies are: {}'.format(request.cookies['test'])) response = text('hello cookies')
response.cookies['right_back'] = 'at you' response.cookies['right_back'] = 'at you'
response.cookies['right_back']['httponly'] = httponly response.cookies['right_back']['httponly'] = httponly
return response return response
@ -43,7 +62,7 @@ def test_false_cookies(httponly, expected):
response_cookies = SimpleCookie() response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {})) response_cookies.load(response.headers.get('Set-Cookie', {}))
'HttpOnly' in response_cookies == expected assert ('HttpOnly' in response_cookies['right_back'].output()) == expected
def test_http2_cookies(): def test_http2_cookies():
app = Sanic('test_http2_cookies') app = Sanic('test_http2_cookies')

View File

@ -0,0 +1,269 @@
from json import JSONDecodeError
from sanic import Sanic
import asyncio
from asyncio import sleep as aio_sleep
from sanic.response import text
from sanic.config import Config
from sanic import server
import aiohttp
from aiohttp import TCPConnector
from sanic.testing import SanicTestClient, HOST, PORT
class ReuseableTCPConnector(TCPConnector):
def __init__(self, *args, **kwargs):
super(ReuseableTCPConnector, self).__init__(*args, **kwargs)
self.old_proto = None
@asyncio.coroutine
def connect(self, req):
new_conn = yield from super(ReuseableTCPConnector, self)\
.connect(req)
if self.old_proto is not None:
if self.old_proto != new_conn._protocol:
raise RuntimeError(
"We got a new connection, wanted the same one!")
print(new_conn.__dict__)
self.old_proto = new_conn._protocol
return new_conn
class ReuseableSanicTestClient(SanicTestClient):
def __init__(self, app, loop=None):
super(ReuseableSanicTestClient, self).__init__(app)
if loop is None:
loop = asyncio.get_event_loop()
self._loop = loop
self._server = None
self._tcp_connector = None
self._session = None
# Copied from SanicTestClient, but with some changes to reuse the
# same loop for the same app.
def _sanic_endpoint_test(
self, method='get', uri='/', gather_request=True,
debug=False, server_kwargs={},
*request_args, **request_kwargs):
loop = self._loop
results = [None, None]
exceptions = []
do_kill_server = request_kwargs.pop('end_server', False)
if gather_request:
def _collect_request(request):
if results[0] is None:
results[0] = request
self.app.request_middleware.appendleft(_collect_request)
@self.app.listener('after_server_start')
async def _collect_response(loop):
try:
if do_kill_server:
request_kwargs['end_session'] = True
response = await self._local_request(
method, uri, *request_args,
**request_kwargs)
results[-1] = response
except Exception as e2:
import traceback
traceback.print_tb(e2.__traceback__)
exceptions.append(e2)
#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)
server.trigger_events(
self.app.listeners['before_server_start'], loop)
try:
loop._stopping = False
http_server = loop.run_until_complete(_server_co)
except Exception as e1:
import traceback
traceback.print_tb(e1.__traceback__)
raise e1
self._server = _server = http_server
server.trigger_events(
self.app.listeners['after_server_start'], loop)
self.app.listeners['after_server_start'].pop()
if do_kill_server:
try:
_server.close()
self._server = None
loop.run_until_complete(_server.wait_closed())
self.app.stop()
except Exception as e3:
import traceback
traceback.print_tb(e3.__traceback__)
exceptions.append(e3)
if exceptions:
raise ValueError(
"Exception during request: {}".format(exceptions))
if gather_request:
self.app.request_middleware.pop()
try:
request, response = results
return request, response
except:
raise ValueError(
"Request and response object expected, got ({})".format(
results))
else:
try:
return results[-1]
except:
raise ValueError(
"Request object expected, got ({})".format(results))
# Copied from SanicTestClient, but with some changes to reuse the
# same TCPConnection and the sane ClientSession more than once.
# Note, you cannot use the same session if you are in a _different_
# loop, so the changes above are required too.
async def _local_request(self, method, uri, cookies=None, *args,
**kwargs):
request_keepalive = kwargs.pop('request_keepalive',
Config.KEEP_ALIVE_TIMEOUT)
if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
url = uri
else:
url = 'http://{host}:{port}{uri}'.format(
host=HOST, port=PORT, uri=uri)
do_kill_session = kwargs.pop('end_session', False)
if self._session:
session = self._session
else:
if self._tcp_connector:
conn = self._tcp_connector
else:
conn = ReuseableTCPConnector(verify_ssl=False,
loop=self._loop,
keepalive_timeout=
request_keepalive)
self._tcp_connector = conn
session = aiohttp.ClientSession(cookies=cookies,
connector=conn,
loop=self._loop)
self._session = session
async with getattr(session, method.lower())(
url, *args, **kwargs) as response:
try:
response.text = await response.text()
except UnicodeDecodeError:
response.text = None
try:
response.json = await response.json()
except (JSONDecodeError,
UnicodeDecodeError,
aiohttp.ClientResponseError):
response.json = None
response.body = await response.read()
if do_kill_session:
session.close()
self._session = None
return response
Config.KEEP_ALIVE_TIMEOUT = 2
Config.KEEP_ALIVE = True
keep_alive_timeout_app_reuse = Sanic('test_ka_timeout_reuse')
keep_alive_app_client_timeout = Sanic('test_ka_client_timeout')
keep_alive_app_server_timeout = Sanic('test_ka_server_timeout')
@keep_alive_timeout_app_reuse.route('/1')
async def handler1(request):
return text('OK')
@keep_alive_app_client_timeout.route('/1')
async def handler2(request):
return text('OK')
@keep_alive_app_server_timeout.route('/1')
async def handler3(request):
return text('OK')
def test_keep_alive_timeout_reuse():
"""If the server keep-alive timeout and client keep-alive timeout are
both longer than the delay, the client _and_ server will successfully
reuse the existing connection."""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop)
headers = {
'Connection': 'keep-alive'
}
request, response = client.get('/1', headers=headers)
assert response.status == 200
assert response.text == 'OK'
loop.run_until_complete(aio_sleep(1))
request, response = client.get('/1', end_server=True)
assert response.status == 200
assert response.text == 'OK'
def test_keep_alive_client_timeout():
"""If the server keep-alive timeout is longer than the client
keep-alive timeout, client will try to create a new connection here."""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_client_timeout,
loop)
headers = {
'Connection': 'keep-alive'
}
request, response = client.get('/1', headers=headers,
request_keepalive=1)
assert response.status == 200
assert response.text == 'OK'
loop.run_until_complete(aio_sleep(2))
exception = None
try:
request, response = client.get('/1', end_server=True,
request_keepalive=1)
except ValueError as e:
exception = e
assert exception is not None
assert isinstance(exception, ValueError)
assert "got a new connection" in exception.args[0]
def test_keep_alive_server_timeout():
"""If the client keep-alive timeout is longer than the server
keep-alive timeout, the client will either a 'Connection reset' error
_or_ a new connection. Depending on how the event-loop handles the
broken server connection."""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_server_timeout,
loop)
headers = {
'Connection': 'keep-alive'
}
request, response = client.get('/1', headers=headers,
request_keepalive=60)
assert response.status == 200
assert response.text == 'OK'
loop.run_until_complete(aio_sleep(3))
exception = None
try:
request, response = client.get('/1', request_keepalive=60,
end_server=True)
except ValueError as e:
exception = e
assert exception is not None
assert isinstance(exception, ValueError)
assert "Connection reset" in exception.args[0] or \
"got a new connection" in exception.args[0]

View File

@ -1,8 +1,8 @@
import uuid import uuid
from importlib import reload from importlib import reload
from sanic.config import LOGGING
from sanic.response import text from sanic.response import text
from sanic.log import LOGGING_CONFIG_DEFAULTS
from sanic import Sanic from sanic import Sanic
from io import StringIO from io import StringIO
import logging import logging
@ -40,18 +40,34 @@ def test_log():
assert rand_string in log_text assert rand_string in log_text
def test_default_log_fmt(): def test_logging_defaults():
reset_logging() reset_logging()
Sanic() app = Sanic("test_logging")
for fmt in [h.formatter for h in logging.getLogger('sanic').handlers]:
assert fmt._fmt == LOGGING['formatters']['simple']['format']
for fmt in [h.formatter for h in logging.getLogger('root').handlers]:
assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['generic']['format']
for fmt in [h.formatter for h in logging.getLogger('sanic.error').handlers]:
assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['generic']['format']
for fmt in [h.formatter for h in logging.getLogger('sanic.access').handlers]:
assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['access']['format']
def test_logging_pass_customer_logconfig():
reset_logging() reset_logging()
Sanic(log_config=None)
for fmt in [h.formatter for h in logging.getLogger('sanic').handlers]:
assert fmt._fmt == "%(asctime)s: %(levelname)s: %(message)s"
modified_config = LOGGING_CONFIG_DEFAULTS
modified_config['formatters']['generic']['format'] = '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s'
modified_config['formatters']['access']['format'] = '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s'
if __name__ == "__main__": app = Sanic("test_logging", log_config=modified_config)
test_log()
for fmt in [h.formatter for h in logging.getLogger('root').handlers]:
assert fmt._fmt == modified_config['formatters']['generic']['format']
for fmt in [h.formatter for h in logging.getLogger('sanic.error').handlers]:
assert fmt._fmt == modified_config['formatters']['generic']['format']
for fmt in [h.formatter for h in logging.getLogger('sanic.access').handlers]:
assert fmt._fmt == modified_config['formatters']['access']['format']

View File

@ -1,38 +1,163 @@
from json import JSONDecodeError
from sanic import Sanic from sanic import Sanic
import asyncio import asyncio
from sanic.response import text from sanic.response import text
from sanic.exceptions import RequestTimeout
from sanic.config import Config from sanic.config import Config
import aiohttp
from aiohttp import TCPConnector
from sanic.testing import SanicTestClient, HOST, PORT
Config.REQUEST_TIMEOUT = 1
request_timeout_app = Sanic('test_request_timeout') class DelayableTCPConnector(TCPConnector):
class RequestContextManager(object):
def __new__(cls, req, delay):
cls = super(DelayableTCPConnector.RequestContextManager, cls).\
__new__(cls)
cls.req = req
cls.send_task = None
cls.resp = None
cls.orig_send = getattr(req, 'send')
cls.orig_start = None
cls.delay = delay
cls._acting_as = req
return cls
def __getattr__(self, item):
acting_as = self._acting_as
return getattr(acting_as, item)
@asyncio.coroutine
def start(self, connection, read_until_eof=False):
if self.send_task is None:
raise RuntimeError("do a send() before you do a start()")
resp = yield from self.send_task
self.send_task = None
self.resp = resp
self._acting_as = self.resp
self.orig_start = getattr(resp, 'start')
try:
ret = yield from self.orig_start(connection,
read_until_eof)
except Exception as e:
raise e
return ret
def close(self):
if self.resp is not None:
self.resp.close()
if self.send_task is not None:
self.send_task.cancel()
@asyncio.coroutine
def delayed_send(self, *args, **kwargs):
req = self.req
if self.delay and self.delay > 0:
#sync_sleep(self.delay)
_ = yield from asyncio.sleep(self.delay)
t = req.loop.time()
print("sending at {}".format(t), flush=True)
conn = next(iter(args)) # first arg is connection
try:
delayed_resp = self.orig_send(*args, **kwargs)
except Exception as e:
return aiohttp.ClientResponse(req.method, req.url)
return delayed_resp
def send(self, *args, **kwargs):
gen = self.delayed_send(*args, **kwargs)
task = self.req.loop.create_task(gen)
self.send_task = task
self._acting_as = task
return self
def __init__(self, *args, **kwargs):
_post_connect_delay = kwargs.pop('post_connect_delay', 0)
_pre_request_delay = kwargs.pop('pre_request_delay', 0)
super(DelayableTCPConnector, self).__init__(*args, **kwargs)
self._post_connect_delay = _post_connect_delay
self._pre_request_delay = _pre_request_delay
@asyncio.coroutine
def connect(self, req):
d_req = DelayableTCPConnector.\
RequestContextManager(req, self._pre_request_delay)
conn = yield from super(DelayableTCPConnector, self).connect(req)
if self._post_connect_delay and self._post_connect_delay > 0:
_ = yield from asyncio.sleep(self._post_connect_delay,
loop=self._loop)
req.send = d_req.send
t = req.loop.time()
print("Connected at {}".format(t), flush=True)
return conn
class DelayableSanicTestClient(SanicTestClient):
def __init__(self, app, loop, request_delay=1):
super(DelayableSanicTestClient, self).__init__(app)
self._request_delay = request_delay
self._loop = None
async def _local_request(self, method, uri, cookies=None, *args,
**kwargs):
if self._loop is None:
self._loop = asyncio.get_event_loop()
if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
url = uri
else:
url = 'http://{host}:{port}{uri}'.format(
host=HOST, port=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,
loop=self._loop) as session:
# Insert a delay after creating the connection
# But before sending the request.
async with getattr(session, method.lower())(
url, *args, **kwargs) as response:
try:
response.text = await response.text()
except UnicodeDecodeError:
response.text = None
try:
response.json = await response.json()
except (JSONDecodeError,
UnicodeDecodeError,
aiohttp.ClientResponseError):
response.json = None
response.body = await response.read()
return response
Config.REQUEST_TIMEOUT = 2
request_timeout_default_app = Sanic('test_request_timeout_default') request_timeout_default_app = Sanic('test_request_timeout_default')
request_no_timeout_app = Sanic('test_request_no_timeout')
@request_timeout_app.route('/1')
async def handler_1(request):
await asyncio.sleep(2)
return text('OK')
@request_timeout_app.exception(RequestTimeout)
def handler_exception(request, exception):
return text('Request Timeout from error_handler.', 408)
def test_server_error_request_timeout():
request, response = request_timeout_app.test_client.get('/1')
assert response.status == 408
assert response.text == 'Request Timeout from error_handler.'
@request_timeout_default_app.route('/1') @request_timeout_default_app.route('/1')
async def handler_2(request): async def handler1(request):
await asyncio.sleep(2) return text('OK')
@request_no_timeout_app.route('/1')
async def handler2(request):
return text('OK') return text('OK')
def test_default_server_error_request_timeout(): def test_default_server_error_request_timeout():
request, response = request_timeout_default_app.test_client.get('/1') client = DelayableSanicTestClient(request_timeout_default_app, None, 3)
request, response = client.get('/1')
assert response.status == 408 assert response.status == 408
assert response.text == 'Error: Request Timeout' assert response.text == 'Error: Request Timeout'
def test_default_server_error_request_dont_timeout():
client = DelayableSanicTestClient(request_no_timeout_app, None, 1)
request, response = client.get('/1')
assert response.status == 200
assert response.text == 'OK'

View File

@ -0,0 +1,38 @@
from sanic import Sanic
import asyncio
from sanic.response import text
from sanic.exceptions import ServiceUnavailable
from sanic.config import Config
Config.RESPONSE_TIMEOUT = 1
response_timeout_app = Sanic('test_response_timeout')
response_timeout_default_app = Sanic('test_response_timeout_default')
@response_timeout_app.route('/1')
async def handler_1(request):
await asyncio.sleep(2)
return text('OK')
@response_timeout_app.exception(ServiceUnavailable)
def handler_exception(request, exception):
return text('Response Timeout from error_handler.', 503)
def test_server_error_response_timeout():
request, response = response_timeout_app.test_client.get('/1')
assert response.status == 503
assert response.text == 'Response Timeout from error_handler.'
@response_timeout_default_app.route('/1')
async def handler_2(request):
await asyncio.sleep(2)
return text('OK')
def test_default_server_error_response_timeout():
request, response = response_timeout_default_app.test_client.get('/1')
assert response.status == 503
assert response.text == 'Error: Response Timeout'

View File

@ -161,3 +161,20 @@ def test_static_content_range_error(file_name, static_file_directory):
assert 'Content-Range' in response.headers assert 'Content-Range' in response.headers
assert response.headers['Content-Range'] == "bytes */%s" % ( assert response.headers['Content-Range'] == "bytes */%s" % (
len(get_file_content(static_file_directory, file_name)),) len(get_file_content(static_file_directory, file_name)),)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
def test_static_file(static_file_directory, file_name):
app = Sanic('test_static')
app.static(
'/testing.file',
get_file_path(static_file_directory, file_name),
host="www.example.com"
)
headers = {"Host": "www.example.com"}
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
request, response = app.test_client.get('/testing.file')
assert response.status == 404

View File

@ -17,6 +17,9 @@ 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='localhost:{}'.format(test_port), _external=True)
URL_FOR_VALUE3 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(test_port) URL_FOR_VALUE3 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(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)
def _generate_handlers_from_names(app, l): def _generate_handlers_from_names(app, l):
@ -49,7 +52,8 @@ def test_simple_url_for_getting(simple_app):
@pytest.mark.parametrize('args,url', @pytest.mark.parametrize('args,url',
[(URL_FOR_ARGS1, URL_FOR_VALUE1), [(URL_FOR_ARGS1, URL_FOR_VALUE1),
(URL_FOR_ARGS2, URL_FOR_VALUE2), (URL_FOR_ARGS2, URL_FOR_VALUE2),
(URL_FOR_ARGS3, URL_FOR_VALUE3)]) (URL_FOR_ARGS3, URL_FOR_VALUE3),
(URL_FOR_ARGS4, URL_FOR_VALUE4)])
def test_simple_url_for_getting_with_more_params(args, url): def test_simple_url_for_getting_with_more_params(args, url):
app = Sanic('more_url_build') app = Sanic('more_url_build')

View File

@ -0,0 +1,446 @@
import inspect
import os
import pytest
from sanic import Sanic
from sanic.blueprints import Blueprint
@pytest.fixture(scope='module')
def static_file_directory():
"""The static directory to serve"""
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
static_directory = os.path.join(current_directory, 'static')
return static_directory
def get_file_path(static_file_directory, file_name):
return os.path.join(static_file_directory, file_name)
def get_file_content(static_file_directory, file_name):
"""The content of the static file to check"""
with open(get_file_path(static_file_directory, file_name), 'rb') as file:
return file.read()
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
def test_static_file(static_file_directory, file_name):
app = Sanic('test_static')
app.static(
'/testing.file', get_file_path(static_file_directory, file_name))
app.static(
'/testing2.file', get_file_path(static_file_directory, file_name),
name='testing_file')
uri = app.url_for('static')
uri2 = app.url_for('static', filename='any')
uri3 = app.url_for('static', name='static', filename='any')
assert uri == '/testing.file'
assert uri == uri2
assert uri2 == uri3
request, response = app.test_client.get(uri)
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
bp = Blueprint('test_bp_static', url_prefix='/bp')
bp.static('/testing.file', get_file_path(static_file_directory, file_name))
bp.static('/testing2.file',
get_file_path(static_file_directory, file_name),
name='testing_file')
app.blueprint(bp)
uri = app.url_for('static', name='test_bp_static.static')
uri2 = app.url_for('static', name='test_bp_static.static', filename='any')
uri3 = app.url_for('test_bp_static.static')
uri4 = app.url_for('test_bp_static.static', name='any')
uri5 = app.url_for('test_bp_static.static', filename='any')
uri6 = app.url_for('test_bp_static.static', name='any', filename='any')
assert uri == '/bp/testing.file'
assert uri == uri2
assert uri2 == uri3
assert uri3 == uri4
assert uri4 == uri5
assert uri5 == uri6
request, response = app.test_client.get(uri)
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
# test for other parameters
uri = app.url_for('static', _external=True, _server='http://localhost')
assert uri == 'http://localhost/testing.file'
uri = app.url_for('static', name='test_bp_static.static',
_external=True, _server='http://localhost')
assert uri == 'http://localhost/bp/testing.file'
# test for defined name
uri = app.url_for('static', name='testing_file')
assert uri == '/testing2.file'
request, response = app.test_client.get(uri)
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
uri = app.url_for('static', name='test_bp_static.testing_file')
assert uri == '/bp/testing2.file'
assert uri == app.url_for('static', name='test_bp_static.testing_file',
filename='any')
request, response = app.test_client.get(uri)
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
@pytest.mark.parametrize('base_uri', ['/static', '', '/dir'])
def test_static_directory(file_name, base_uri, static_file_directory):
app = Sanic('test_static')
app.static(base_uri, static_file_directory)
base_uri2 = base_uri + '/2'
app.static(base_uri2, static_file_directory, name='uploads')
uri = app.url_for('static', name='static', filename=file_name)
assert uri == '{}/{}'.format(base_uri, file_name)
request, response = app.test_client.get(uri)
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
uri2 = app.url_for('static', name='static', filename='/' + file_name)
uri3 = app.url_for('static', filename=file_name)
uri4 = app.url_for('static', filename='/' + file_name)
uri5 = app.url_for('static', name='uploads', filename=file_name)
uri6 = app.url_for('static', name='uploads', filename='/' + file_name)
assert uri == uri2
assert uri2 == uri3
assert uri3 == uri4
assert uri5 == '{}/{}'.format(base_uri2, file_name)
assert uri5 == uri6
bp = Blueprint('test_bp_static', url_prefix='/bp')
bp.static(base_uri, static_file_directory)
bp.static(base_uri2, static_file_directory, name='uploads')
app.blueprint(bp)
uri = app.url_for('static', name='test_bp_static.static',
filename=file_name)
uri2 = app.url_for('static', name='test_bp_static.static',
filename='/' + file_name)
uri4 = app.url_for('static', name='test_bp_static.uploads',
filename=file_name)
uri5 = app.url_for('static', name='test_bp_static.uploads',
filename='/' + file_name)
assert uri == '/bp{}/{}'.format(base_uri, file_name)
assert uri == uri2
assert uri4 == '/bp{}/{}'.format(base_uri2, file_name)
assert uri4 == uri5
request, response = app.test_client.get(uri)
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_head_request(file_name, static_file_directory):
app = Sanic('test_static')
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
bp = Blueprint('test_bp_static', url_prefix='/bp')
bp.static('/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
app.blueprint(bp)
uri = app.url_for('static')
assert uri == '/testing.file'
assert uri == app.url_for('static', name='static')
assert uri == app.url_for('static', name='static', filename='any')
request, response = app.test_client.head(uri)
assert response.status == 200
assert 'Accept-Ranges' in response.headers
assert 'Content-Length' in response.headers
assert int(response.headers[
'Content-Length']) == len(
get_file_content(static_file_directory, file_name))
# blueprint
uri = app.url_for('static', name='test_bp_static.static')
assert uri == '/bp/testing.file'
assert uri == app.url_for('static', name='test_bp_static.static',
filename='any')
request, response = app.test_client.head(uri)
assert response.status == 200
assert 'Accept-Ranges' in response.headers
assert 'Content-Length' in response.headers
assert int(response.headers[
'Content-Length']) == len(
get_file_content(static_file_directory, file_name))
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_correct(file_name, static_file_directory):
app = Sanic('test_static')
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
bp = Blueprint('test_bp_static', url_prefix='/bp')
bp.static('/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
app.blueprint(bp)
headers = {
'Range': 'bytes=12-19'
}
uri = app.url_for('static')
assert uri == '/testing.file'
assert uri == app.url_for('static', name='static')
assert uri == app.url_for('static', name='static', filename='any')
request, response = app.test_client.get(uri, headers=headers)
assert response.status == 200
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
static_file_directory, file_name))[12:19]
assert int(response.headers[
'Content-Length']) == len(static_content)
assert response.body == static_content
# blueprint
uri = app.url_for('static', name='test_bp_static.static')
assert uri == '/bp/testing.file'
assert uri == app.url_for('static', name='test_bp_static.static',
filename='any')
assert uri == app.url_for('test_bp_static.static')
assert uri == app.url_for('test_bp_static.static', name='any')
assert uri == app.url_for('test_bp_static.static', filename='any')
assert uri == app.url_for('test_bp_static.static', name='any',
filename='any')
request, response = app.test_client.get(uri, headers=headers)
assert response.status == 200
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
static_file_directory, file_name))[12:19]
assert int(response.headers[
'Content-Length']) == len(static_content)
assert response.body == static_content
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_front(file_name, static_file_directory):
app = Sanic('test_static')
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
bp = Blueprint('test_bp_static', url_prefix='/bp')
bp.static('/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
app.blueprint(bp)
headers = {
'Range': 'bytes=12-'
}
uri = app.url_for('static')
assert uri == '/testing.file'
assert uri == app.url_for('static', name='static')
assert uri == app.url_for('static', name='static', filename='any')
request, response = app.test_client.get(uri, headers=headers)
assert response.status == 200
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
static_file_directory, file_name))[12:]
assert int(response.headers[
'Content-Length']) == len(static_content)
assert response.body == static_content
# blueprint
uri = app.url_for('static', name='test_bp_static.static')
assert uri == '/bp/testing.file'
assert uri == app.url_for('static', name='test_bp_static.static',
filename='any')
assert uri == app.url_for('test_bp_static.static')
assert uri == app.url_for('test_bp_static.static', name='any')
assert uri == app.url_for('test_bp_static.static', filename='any')
assert uri == app.url_for('test_bp_static.static', name='any',
filename='any')
request, response = app.test_client.get(uri, headers=headers)
assert response.status == 200
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
static_file_directory, file_name))[12:]
assert int(response.headers[
'Content-Length']) == len(static_content)
assert response.body == static_content
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_back(file_name, static_file_directory):
app = Sanic('test_static')
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
bp = Blueprint('test_bp_static', url_prefix='/bp')
bp.static('/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
app.blueprint(bp)
headers = {
'Range': 'bytes=-12'
}
uri = app.url_for('static')
assert uri == '/testing.file'
assert uri == app.url_for('static', name='static')
assert uri == app.url_for('static', name='static', filename='any')
request, response = app.test_client.get(uri, headers=headers)
assert response.status == 200
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
static_file_directory, file_name))[-12:]
assert int(response.headers[
'Content-Length']) == len(static_content)
assert response.body == static_content
# blueprint
uri = app.url_for('static', name='test_bp_static.static')
assert uri == '/bp/testing.file'
assert uri == app.url_for('static', name='test_bp_static.static',
filename='any')
assert uri == app.url_for('test_bp_static.static')
assert uri == app.url_for('test_bp_static.static', name='any')
assert uri == app.url_for('test_bp_static.static', filename='any')
assert uri == app.url_for('test_bp_static.static', name='any',
filename='any')
request, response = app.test_client.get(uri, headers=headers)
assert response.status == 200
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
static_file_directory, file_name))[-12:]
assert int(response.headers[
'Content-Length']) == len(static_content)
assert response.body == static_content
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_empty(file_name, static_file_directory):
app = Sanic('test_static')
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
bp = Blueprint('test_bp_static', url_prefix='/bp')
bp.static('/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
app.blueprint(bp)
uri = app.url_for('static')
assert uri == '/testing.file'
assert uri == app.url_for('static', name='static')
assert uri == app.url_for('static', name='static', filename='any')
request, response = app.test_client.get(uri)
assert response.status == 200
assert 'Content-Length' in response.headers
assert 'Content-Range' not in response.headers
assert int(response.headers[
'Content-Length']) == len(get_file_content(static_file_directory, file_name))
assert response.body == bytes(
get_file_content(static_file_directory, file_name))
# blueprint
uri = app.url_for('static', name='test_bp_static.static')
assert uri == '/bp/testing.file'
assert uri == app.url_for('static', name='test_bp_static.static',
filename='any')
assert uri == app.url_for('test_bp_static.static')
assert uri == app.url_for('test_bp_static.static', name='any')
assert uri == app.url_for('test_bp_static.static', filename='any')
assert uri == app.url_for('test_bp_static.static', name='any',
filename='any')
request, response = app.test_client.get(uri)
assert response.status == 200
assert 'Content-Length' in response.headers
assert 'Content-Range' not in response.headers
assert int(response.headers[
'Content-Length']) == len(get_file_content(static_file_directory, file_name))
assert response.body == bytes(
get_file_content(static_file_directory, file_name))
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_error(file_name, static_file_directory):
app = Sanic('test_static')
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
bp = Blueprint('test_bp_static', url_prefix='/bp')
bp.static('/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
app.blueprint(bp)
headers = {
'Range': 'bytes=1-0'
}
uri = app.url_for('static')
assert uri == '/testing.file'
assert uri == app.url_for('static', name='static')
assert uri == app.url_for('static', name='static', filename='any')
request, response = app.test_client.get(uri, headers=headers)
assert response.status == 416
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
assert response.headers['Content-Range'] == "bytes */%s" % (
len(get_file_content(static_file_directory, file_name)),)
# blueprint
uri = app.url_for('static', name='test_bp_static.static')
assert uri == '/bp/testing.file'
assert uri == app.url_for('static', name='test_bp_static.static',
filename='any')
assert uri == app.url_for('test_bp_static.static')
assert uri == app.url_for('test_bp_static.static', name='any')
assert uri == app.url_for('test_bp_static.static', filename='any')
assert uri == app.url_for('test_bp_static.static', name='any',
filename='any')
request, response = app.test_client.get(uri, headers=headers)
assert response.status == 416
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
assert response.headers['Content-Range'] == "bytes */%s" % (
len(get_file_content(static_file_directory, file_name)),)