Merge pull request #1 from channelcat/master

Update
This commit is contained in:
howie.hu 2017-12-20 09:08:52 +08:00 committed by GitHub
commit 68cb280513
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 2941 additions and 621 deletions

1
.gitignore vendored
View File

@ -14,3 +14,4 @@ settings.py
docs/_build/ docs/_build/
docs/_api/ docs/_api/
build/* build/*
.DS_Store

View File

@ -1,4 +1,5 @@
sudo: false sudo: false
dist: precise
language: python language: python
cache: cache:
directories: directories:

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) [year] [fullname] Copyright (c) 2016-present Channel Cat
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.

View File

@ -1,4 +1,7 @@
include README.rst include README.rst
include MANIFEST.in
include LICENSE
include setup.py
recursive-exclude * __pycache__ recursive-exclude * __pycache__
recursive-exclude * *.py[co] recursive-exclude * *.py[co]

View File

@ -31,13 +31,13 @@ Hello World Example
Installation Installation
------------ ------------
- ``python -m pip install sanic`` - ``pip install sanic``
To install sanic without uvloop or json using bash, you can provide either or both of these environmental variables To install sanic without uvloop or json using bash, you can provide either or both of these environmental variables
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the NO_X to true will stop that features using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the NO_X to true will stop that features
installation. installation.
- ``SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true python -m pip install sanic`` - ``SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip install sanic``
Documentation Documentation
@ -55,6 +55,16 @@ Documentation
:target: https://pypi.python.org/pypi/sanic/ :target: https://pypi.python.org/pypi/sanic/
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg .. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
:target: https://pypi.python.org/pypi/sanic/ :target: https://pypi.python.org/pypi/sanic/
Examples
--------
`Non-Core examples <https://github.com/channelcat/sanic/wiki/Examples/>`_. Examples of plugins and Sanic that are outside the scope of Sanic core.
`Extensions <https://github.com/channelcat/sanic/wiki/Extensions/>`_. Sanic extensions created by the community.
`Projects <https://github.com/channelcat/sanic/wiki/Projects/>`_. Sanic in production use.
TODO TODO
---- ----

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
@ -172,7 +179,7 @@ takes the format `<blueprint_name>.<handler_name>`. For example:
```python ```python
@blueprint_v1.route('/') @blueprint_v1.route('/')
async def root(request): async def root(request):
url = app.url_for('v1.post_handler', post_id=5) # --> '/v1/post/5' url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/post/5'
return redirect(url) return redirect(url)

View File

@ -29,9 +29,15 @@ In general the convention is to only have UPPERCASE configuration parameters. Th
There are several ways how to load configuration. There are several ways how to load configuration.
### From environment variables. ### From Environment Variables
Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically. You can pass the `load_env` boolean to the Sanic constructor to override that: Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically and fed into the `REQUEST_TIMEOUT` config variable. You can pass a different prefix to Sanic:
```python
app = Sanic(load_env='MYAPP_')
```
Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable loading from environment variables you can set it to `False` instead:
```python ```python
app = Sanic(load_env=False) app = Sanic(load_env=False)
@ -79,8 +85,35 @@ DB_USER = 'appuser'
Out of the box there are just a few predefined values which can be overwritten when creating the application. Out of the box there are just a few predefined values which can be overwritten when creating the application.
| Variable | Default | Description | | Variable | Default | Description |
| ----------------- | --------- | --------------------------------- | | ------------------ | --------- | --------------------------------------------- |
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) | | REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) | | REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
| KEEP_ALIVE | True | Disables keep-alive when False | | RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
| KEEP_ALIVE | True | Disables keep-alive when False |
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
### The different Timeout variables:
A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the `REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates a HTTP 408 response and sends that to the client. Adjust this value higher if your clients routinely pass very large request payloads or upload requests very slowly.
A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT` value (in seconds), this is considered a Server Error so Sanic generates a HTTP 503 response and sets that to the client. Adjust this value higher if your application is likely to have long-running process that delay the generation of a response.
### What is Keep Alive? And what does the Keep Alive Timeout value do?
Keep-Alive is a HTTP feature indroduced in HTTP 1.1. When sending a HTTP request, the client (usually a web browser application) can set a Keep-Alive header to indicate for the http server (Sanic) to not close the TCP connection after it has send the response. This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient network traffic for both the client and the server.
The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application, set it to `False` to cause all client connections to close immediately after a response is sent, regardless of the Keep-Alive header on the request.
The amount of time the server holds the TCP connection open is decided by the server itself. In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds, this is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless you know your clients are using a browser which supports TCP connections held open for that long.
For reference:
```
Apache httpd server default keepalive timeout = 5 seconds
Nginx server default keepalive timeout = 75 seconds
Nginx performance tuning guidelines uses keepalive = 15 seconds
IE (5-9) client hard keepalive limit = 60 seconds
Firefox client hard keepalive limit = 115 seconds
Opera 11 client hard keepalive limit = 120 seconds
Chrome 13+ client keepalive limit > 300+ seconds
```

View File

@ -1,12 +1,13 @@
# Extensions # Extensions
A list of Sanic extensions created by the community. A list of Sanic extensions created by the community.
- [Sanic-Plugins-Framework](https://github.com/ashleysommer/sanicpluginsframework): Library for easily creating and using Sanic plugins.
- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions. - [Sessions](https://github.com/subyraman/sanic_session): Support for sessions.
Allows using redis, memcache or an in memory store. Allows using redis, memcache or an in memory store.
- [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

@ -4,7 +4,7 @@ Middleware are functions which are executed before or after requests to the
server. They can be used to modify the *request to* or *response from* server. They can be used to modify the *request to* or *response from*
user-defined handler functions. user-defined handler functions.
Additionally, Sanic providers listeners which allow you to run code at various points of your application's lifecycle. Additionally, Sanic provides listeners which allow you to run code at various points of your application's lifecycle.
## Middleware ## Middleware

View File

@ -71,8 +71,14 @@ The following variables are accessible as properties on `Request` objects:
return text("You are trying to create a user with the following POST: %s" % request.body) return text("You are trying to create a user with the following POST: %s" % request.body)
``` ```
- `headers` (dict) - A case-insensitive dictionary that contains the request headers.
- `ip` (str) - IP address of the requester. - `ip` (str) - IP address of the requester.
- `port` (str) - Port address of the requester.
- `socket` (tuple) - (IP, port) of the requester.
- `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object. - `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object.
```python ```python
@ -95,6 +101,7 @@ The following variables are accessible as properties on `Request` objects:
- `path`: The path of the request: `/posts/1/` - `path`: The path of the request: `/posts/1/`
- `query_string`: The query string of the request: `foo=bar` or a blank string `''` - `query_string`: The query string of the request: `foo=bar` or a blank string `''`
- `uri_template`: Template for matching route handler: `/posts/<id>/` - `uri_template`: Template for matching route handler: `/posts/<id>/`
- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=`
## Accessing values using `get` and `getlist` ## Accessing values using `get` and `getlist`

View File

@ -214,3 +214,121 @@ and `recv` methods to send and receive data respectively.
WebSocket support requires the [websockets](https://github.com/aaugustin/websockets) WebSocket support requires the [websockets](https://github.com/aaugustin/websockets)
package by Aymeric Augustin. package by Aymeric Augustin.
## About `strict_slashes`
You can make `routes` strict to trailing slash or not, it's configurable.
```python
# provide default strict_slashes value for all routes
app = Sanic('test_route_strict_slash', strict_slashes=True)
# you can also overwrite strict_slashes value for specific route
@app.get('/get', strict_slashes=False)
def handler(request):
return text('OK')
# It also works for blueprints
bp = Blueprint('test_bp_strict_slash', strict_slashes=True)
@bp.get('/bp/get', strict_slashes=False)
def handler(request):
return text('OK')
app.blueprint(bp)
```
## User defined route name
You can pass `name` to change the route name to avoid using the default name (`handler.__name__`).
```python
app = Sanic('test_named_route')
@app.get('/get', name='get_handler')
def handler(request):
return text('OK')
# then you need use `app.url_for('get_handler')`
# instead of # `app.url_for('handler')`
# It also works for blueprints
bp = Blueprint('test_named_bp')
@bp.get('/bp/get', name='get_handler')
def handler(request):
return text('OK')
app.blueprint(bp)
# then you need use `app.url_for('test_named_bp.get_handler')`
# instead of `app.url_for('test_named_bp.handler')`
# different names can be used for same url with different methods
@app.get('/test', name='route_test')
def handler(request):
return text('OK')
@app.post('/test', name='route_post')
def handler2(request):
return text('OK POST')
@app.put('/test', name='route_put')
def handler3(request):
return text('OK PUT')
# below url are the same, you can use any of them
# '/test'
app.url_for('route_test')
# app.url_for('route_post')
# app.url_for('route_put')
# for same handler name with different methods
# you need specify the name (it's url_for issue)
@app.get('/get')
def handler(request):
return text('OK')
@app.post('/post', name='post_handler')
def handler(request):
return text('OK')
# then
# app.url_for('handler') == '/get'
# 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`.

View File

@ -20,7 +20,7 @@ def test_index_put_not_allowed():
assert response.status == 405 assert response.status == 405
``` ```
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.01:42101` and Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and
your test request is executed against your application, using `aiohttp`. your test request is executed against your application, using `aiohttp`.
The `test_client` methods accept the following arguments and keyword arguments: The `test_client` methods accept the following arguments and keyword arguments:
@ -59,7 +59,7 @@ the available arguments to aiohttp can be found
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session). [in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
# pytest-sanic ## pytest-sanic
[pytest-sanic](https://github.com/yunstanford/pytest-sanic) is a pytest plugin, it helps you to test your code asynchronously. [pytest-sanic](https://github.com/yunstanford/pytest-sanic) is a pytest plugin, it helps you to test your code asynchronously.
Just write tests like, Just write tests like,

View File

@ -10,11 +10,11 @@ You can pass a version number to the routes directly.
from sanic import response from sanic import response
@app.route('/text', verion=1) @app.route('/text', version=1)
def handle_request(request): def handle_request(request):
return response.text('Hello world! Version 1') return response.text('Hello world! Version 1')
@app.route('/text', verion=2) @app.route('/text', version=2)
def handle_request(request): def handle_request(request):
return response.text('Hello world! Version 2') return response.text('Hello world! Version 2')

View File

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
import asyncio
from sanic import Sanic
app = Sanic()
async def notify_server_started_after_five_seconds():
await asyncio.sleep(5)
print('Server successfully started!')
app.add_task(notify_server_started_after_five_seconds())
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
from sanic import Sanic
from functools import wraps
from sanic.response import json
app = Sanic()
def check_request_for_authorization_status(request):
# Note: Define your check, for instance cookie, session.
flag = True
return flag
def authorized():
def decorator(f):
@wraps(f)
async def decorated_function(request, *args, **kwargs):
# run some method that checks the request
# for the client's authorization status
is_authorized = check_request_for_authorization_status(request)
if is_authorized:
# the user is authorized.
# run the handler method and return the response
response = await f(request, *args, **kwargs)
return response
else:
# the user is not authorized.
return json({'status': 'not_authorized'}, 403)
return decorated_function
return decorator
@app.route("/")
@authorized()
async def test(request):
return json({'status': 'authorized'})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

View File

@ -0,0 +1,86 @@
'''
Based on example from https://github.com/Skyscanner/aiotask-context
and `examples/{override_logging,run_async}.py`.
Needs https://github.com/Skyscanner/aiotask-context/tree/52efbc21e2e1def2d52abb9a8e951f3ce5e6f690 or newer
$ pip install git+https://github.com/Skyscanner/aiotask-context.git
'''
import asyncio
import uuid
import logging
from signal import signal, SIGINT
from sanic import Sanic
from sanic import response
import uvloop
import aiotask_context as context
log = logging.getLogger(__name__)
class RequestIdFilter(logging.Filter):
def filter(self, record):
record.request_id = context.get('X-Request-ID')
return True
LOG_SETTINGS = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
'formatter': 'default',
'filters': ['requestid'],
},
},
'filters': {
'requestid': {
'()': RequestIdFilter,
},
},
'formatters': {
'default': {
'format': '%(asctime)s %(levelname)s %(name)s:%(lineno)d %(request_id)s | %(message)s',
},
},
'loggers': {
'': {
'level': 'DEBUG',
'handlers': ['console'],
'propagate': True
},
}
}
app = Sanic(__name__, log_config=LOG_SETTINGS)
@app.middleware('request')
async def set_request_id(request):
request_id = request.headers.get('X-Request-ID') or str(uuid.uuid4())
context.set("X-Request-ID", request_id)
@app.route("/")
async def test(request):
log.debug('X-Request-ID: %s', context.get('X-Request-ID'))
log.info('Hello from test!')
return response.json({"test": True})
if __name__ == '__main__':
asyncio.set_event_loop(uvloop.new_event_loop())
server = app.create_server(host="0.0.0.0", port=8000)
loop = asyncio.get_event_loop()
loop.set_task_factory(context.task_factory)
task = asyncio.ensure_future(server)
try:
loop.run_forever()
except:
loop.stop()

49
examples/pytest_xdist.py Normal file
View File

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

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

@ -18,7 +18,7 @@ def test_sync(request):
return response.json({"test": True}) return response.json({"test": True})
@app.route("/dynamic/<name>/<id:int>") @app.route("/dynamic/<name>/<i:int>")
def test_params(request, name, i): def test_params(request, name, i):
return response.text("yeehaww {} {}".format(name, i)) return response.text("yeehaww {} {}".format(name, i))

View File

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

View File

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

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,43 +28,36 @@ 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, log_config=None,
if log_config: configure_logging=True):
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 and
log_config):
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
if configure_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 = {}
self._blueprint_order = [] self._blueprint_order = []
self.configure_logging = configure_logging
self.debug = None self.debug = None
self.sock = None self.sock = None
self.strict_slashes = strict_slashes
self.listeners = defaultdict(list) self.listeners = defaultdict(list)
self.is_running = False self.is_running = False
self.is_request_stream = False self.is_request_stream = False
self.websocket_enabled = False self.websocket_enabled = False
self.websocket_tasks = [] self.websocket_tasks = set()
# Register alternative method names # Register alternative method names
self.go_fast = self.run self.go_fast = self.run
@ -113,7 +106,7 @@ class Sanic:
# Decorator # Decorator
def route(self, uri, methods=frozenset({'GET'}), host=None, def route(self, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False, stream=False, version=None): strict_slashes=None, stream=False, version=None, name=None):
"""Decorate a function to be registered as a route """Decorate a function to be registered as a route
:param uri: path of the URL :param uri: path of the URL
@ -121,6 +114,8 @@ class Sanic:
:param host: :param host:
:param strict_slashes: :param strict_slashes:
:param stream: :param stream:
:param version:
:param name: user defined route name for url_for
:return: decorated function :return: decorated function
""" """
@ -132,53 +127,64 @@ class Sanic:
if stream: if stream:
self.is_request_stream = True self.is_request_stream = True
if strict_slashes is None:
strict_slashes = self.strict_slashes
def response(handler): def response(handler):
if stream: if stream:
handler.is_stream = stream handler.is_stream = stream
self.router.add(uri=uri, methods=methods, handler=handler, self.router.add(uri=uri, methods=methods, handler=handler,
host=host, strict_slashes=strict_slashes, host=host, strict_slashes=strict_slashes,
version=version) version=version, name=name)
return handler return handler
return response return response
# Shorthand method decorators # Shorthand method decorators
def get(self, uri, host=None, strict_slashes=False, version=None): def get(self, uri, host=None, strict_slashes=None, version=None,
name=None):
return self.route(uri, methods=frozenset({"GET"}), host=host, return self.route(uri, methods=frozenset({"GET"}), host=host,
strict_slashes=strict_slashes, version=version) strict_slashes=strict_slashes, version=version,
name=name)
def post(self, uri, host=None, strict_slashes=False, stream=False, def post(self, uri, host=None, strict_slashes=None, stream=False,
version=None): version=None, name=None):
return self.route(uri, methods=frozenset({"POST"}), host=host, return self.route(uri, methods=frozenset({"POST"}), host=host,
strict_slashes=strict_slashes, stream=stream, strict_slashes=strict_slashes, stream=stream,
version=version) version=version, name=name)
def put(self, uri, host=None, strict_slashes=False, stream=False, def put(self, uri, host=None, strict_slashes=None, stream=False,
version=None): version=None, name=None):
return self.route(uri, methods=frozenset({"PUT"}), host=host, return self.route(uri, methods=frozenset({"PUT"}), host=host,
strict_slashes=strict_slashes, stream=stream, strict_slashes=strict_slashes, stream=stream,
version=version) version=version, name=name)
def head(self, uri, host=None, strict_slashes=False, version=None): def head(self, uri, host=None, strict_slashes=None, version=None,
name=None):
return self.route(uri, methods=frozenset({"HEAD"}), host=host, return self.route(uri, methods=frozenset({"HEAD"}), host=host,
strict_slashes=strict_slashes, version=version) strict_slashes=strict_slashes, version=version,
name=name)
def options(self, uri, host=None, strict_slashes=False, version=None): def options(self, uri, host=None, strict_slashes=None, version=None,
name=None):
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host, return self.route(uri, methods=frozenset({"OPTIONS"}), host=host,
strict_slashes=strict_slashes, version=version) strict_slashes=strict_slashes, version=version,
name=name)
def patch(self, uri, host=None, strict_slashes=False, stream=False, def patch(self, uri, host=None, strict_slashes=None, stream=False,
version=None): version=None, name=None):
return self.route(uri, methods=frozenset({"PATCH"}), host=host, return self.route(uri, methods=frozenset({"PATCH"}), host=host,
strict_slashes=strict_slashes, stream=stream, strict_slashes=strict_slashes, stream=stream,
version=version) version=version, name=name)
def delete(self, uri, host=None, strict_slashes=False, version=None): def delete(self, uri, host=None, strict_slashes=None, version=None,
name=None):
return self.route(uri, methods=frozenset({"DELETE"}), host=host, return self.route(uri, methods=frozenset({"DELETE"}), host=host,
strict_slashes=strict_slashes, version=version) strict_slashes=strict_slashes, version=version,
name=name)
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False, version=None): strict_slashes=None, version=None, name=None):
"""A helper method to register class instance or """A helper method to register class instance or
functions as a handler to the application url functions as a handler to the application url
routes. routes.
@ -188,6 +194,9 @@ class Sanic:
:param methods: list or tuple of methods allowed, these are overridden :param methods: list or tuple of methods allowed, these are overridden
if using a HTTPMethodView if using a HTTPMethodView
:param host: :param host:
:param strict_slashes:
:param version:
:param name: user defined route name for url_for
:return: function or class instance :return: function or class instance
""" """
stream = False stream = False
@ -210,15 +219,21 @@ class Sanic:
stream = True stream = True
break break
if strict_slashes is None:
strict_slashes = self.strict_slashes
self.route(uri=uri, methods=methods, host=host, self.route(uri=uri, methods=methods, host=host,
strict_slashes=strict_slashes, stream=stream, strict_slashes=strict_slashes, stream=stream,
version=version)(handler) version=version, name=name)(handler)
return handler return handler
# Decorator # Decorator
def websocket(self, uri, host=None, strict_slashes=False): def websocket(self, uri, host=None, strict_slashes=None,
subprotocols=None, name=None):
"""Decorate a function to be registered as a websocket route """Decorate a function to be registered as a websocket route
:param uri: path of the URL :param uri: path of the URL
:param subprotocols: optional list of strings with the supported
subprotocols
:param host: :param host:
:return: decorated function :return: decorated function
""" """
@ -229,6 +244,9 @@ class Sanic:
if not uri.startswith('/'): if not uri.startswith('/'):
uri = '/' + uri uri = '/' + uri
if strict_slashes is None:
strict_slashes = self.strict_slashes
def response(handler): def response(handler):
async def websocket_handler(request, *args, **kwargs): async def websocket_handler(request, *args, **kwargs):
request.app = self request.app = self
@ -238,13 +256,13 @@ class Sanic:
# On Python3.5 the Transport classes in asyncio do not # On Python3.5 the Transport classes in asyncio do not
# have a get_protocol() method as in uvloop # have a get_protocol() method as in uvloop
protocol = request.transport._protocol protocol = request.transport._protocol
ws = await protocol.websocket_handshake(request) ws = await protocol.websocket_handshake(request, subprotocols)
# schedule the application handler # schedule the application handler
# its future is kept in self.websocket_tasks in case it # its future is kept in self.websocket_tasks in case it
# needs to be cancelled due to the server being stopped # needs to be cancelled due to the server being stopped
fut = ensure_future(handler(request, ws, *args, **kwargs)) fut = ensure_future(handler(request, ws, *args, **kwargs))
self.websocket_tasks.append(fut) self.websocket_tasks.add(fut)
try: try:
await fut await fut
except (CancelledError, ConnectionClosed): except (CancelledError, ConnectionClosed):
@ -254,16 +272,19 @@ class Sanic:
self.router.add(uri=uri, handler=websocket_handler, self.router.add(uri=uri, handler=websocket_handler,
methods=frozenset({'GET'}), host=host, methods=frozenset({'GET'}), host=host,
strict_slashes=strict_slashes) strict_slashes=strict_slashes, name=name)
return handler return handler
return response return response
def add_websocket_route(self, handler, uri, host=None, def add_websocket_route(self, handler, uri, host=None,
strict_slashes=False): strict_slashes=None, name=None):
"""A helper method to register a function as a websocket route.""" """A helper method to register a function as a websocket route."""
return self.websocket(uri, host=host, if strict_slashes is None:
strict_slashes=strict_slashes)(handler) strict_slashes = self.strict_slashes
return self.websocket(uri, host=host, strict_slashes=strict_slashes,
name=name)(handler)
def enable_websocket(self, enable=True): def enable_websocket(self, enable=True):
"""Enable or disable the support for websocket. """Enable or disable the support for websocket.
@ -327,13 +348,14 @@ class Sanic:
# Static Files # Static Files
def static(self, uri, file_or_directory, pattern=r'/?.+', def static(self, uri, file_or_directory, pattern=r'/?.+',
use_modified_since=True, use_content_range=False, use_modified_since=True, use_content_range=False,
stream_large_files=False): stream_large_files=False, name='static', host=None,
strict_slashes=None):
"""Register a root to serve files from. The input can either be a """Register a root to serve files from. The input can either be a
file or a directory. See file or a directory. See
""" """
static_register(self, uri, file_or_directory, pattern, static_register(self, uri, file_or_directory, pattern,
use_modified_since, use_content_range, use_modified_since, use_content_range,
stream_large_files) stream_large_files, name, host, strict_slashes)
def blueprint(self, blueprint, **options): def blueprint(self, blueprint, **options):
"""Register a blueprint on the application. """Register a blueprint on the application.
@ -383,12 +405,31 @@ 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)
raise URLBuildError( if not (uri and route):
'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]
@ -412,6 +453,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)
@ -493,6 +544,7 @@ class Sanic:
# Fetch handler from router # Fetch handler from router
handler, args, kwargs, uri = self.router.get(request) handler, args, kwargs, uri = self.router.get(request)
request.uri_template = uri request.uri_template = uri
if handler is None: if handler is None:
raise ServerError( raise ServerError(
@ -527,9 +579,9 @@ class Sanic:
try: try:
response = await self._run_response_middleware(request, response = await self._run_response_middleware(request,
response) response)
except: except BaseException:
log.exception( error_logger.exception(
'Exception occured in one of response middleware handlers' 'Exception occurred in one of response middleware handlers'
) )
# pass the response to the correct callback # pass the response to the correct callback
@ -553,7 +605,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.
@ -571,12 +623,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)
@ -589,7 +639,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
@ -597,13 +647,13 @@ class Sanic:
serve(**server_settings) serve(**server_settings)
else: else:
serve_multiple(server_settings, workers) serve_multiple(server_settings, workers)
except: except BaseException:
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"""
@ -616,17 +666,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)
@ -635,14 +684,31 @@ class Sanic:
warnings.simplefilter('default') warnings.simplefilter('default')
warnings.warn("stop_event will be removed from future versions.", warnings.warn("stop_event will be removed from future versions.",
DeprecationWarning) DeprecationWarning)
server_settings = self._helper( server_settings = self._helper(
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
await self.trigger_events(
server_settings.get('before_start', []),
server_settings.get('loop')
)
return await serve(**server_settings) return await serve(**server_settings)
async def trigger_events(self, events, loop):
"""Trigger events (functions or async)
:param events: one or more sync or async functions to execute
:param loop: event loop
"""
for event in events:
result = event(loop)
if isawaitable(result):
await result
async def _run_request_middleware(self, request): async def _run_request_middleware(self, request):
# The if improves speed. I don't know why # The if improves speed. I don't know why
if self.request_middleware: if self.request_middleware:
@ -668,7 +734,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
@ -702,12 +768,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
@ -730,10 +798,10 @@ class Sanic:
listeners = [partial(listener, self) for listener in listeners] listeners = [partial(listener, self) for listener in listeners]
server_settings[settings_name] = listeners server_settings[settings_name] = listeners
if debug: if self.configure_logging and debug:
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
@ -743,6 +811,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

@ -5,7 +5,7 @@ from sanic.views import CompositionView
FutureRoute = namedtuple('Route', FutureRoute = namedtuple('Route',
['handler', 'uri', 'methods', 'host', ['handler', 'uri', 'methods', 'host',
'strict_slashes', 'stream', 'version']) 'strict_slashes', 'stream', 'version', 'name'])
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host']) FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs']) FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs']) FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
@ -14,11 +14,16 @@ FutureStatic = namedtuple('Route',
class Blueprint: class Blueprint:
def __init__(self, name, url_prefix=None, host=None, version=None):
def __init__(self, name,
url_prefix=None,
host=None, version=None,
strict_slashes=False):
"""Create a new blueprint """Create a new blueprint
:param name: unique name of the blueprint :param name: unique name of the blueprint
:param url_prefix: URL to be prefixed before all route URLs :param url_prefix: URL to be prefixed before all route URLs
:param strict_slashes: strict to trailing slash
""" """
self.name = name self.name = name
self.url_prefix = url_prefix self.url_prefix = url_prefix
@ -31,6 +36,7 @@ class Blueprint:
self.middlewares = [] self.middlewares = []
self.statics = [] self.statics = []
self.version = version self.version = version
self.strict_slashes = strict_slashes
def register(self, app, options): def register(self, app, options):
"""Register the blueprint to the sanic app.""" """Register the blueprint to the sanic app."""
@ -47,14 +53,14 @@ class Blueprint:
version = future.version or self.version version = future.version or self.version
app.route( app.route(uri=uri[1:] if uri.startswith('//') else uri,
uri=uri[1:] if uri.startswith('//') else uri, methods=future.methods,
methods=future.methods, host=future.host or self.host,
host=future.host or self.host, strict_slashes=future.strict_slashes,
strict_slashes=future.strict_slashes, stream=future.stream,
stream=future.stream, version=version,
version=version name=future.name,
)(future.handler) )(future.handler)
for future in self.websocket_routes: for future in self.websocket_routes:
# attach the blueprint name to the handler so that it can be # attach the blueprint name to the handler so that it can be
@ -62,11 +68,11 @@ class Blueprint:
future.handler.__blueprintname__ = self.name future.handler.__blueprintname__ = self.name
# Prepend the blueprint URI prefix if available # Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri uri = url_prefix + future.uri if url_prefix else future.uri
app.websocket( app.websocket(uri=uri,
uri=uri, host=future.host or self.host,
host=future.host or self.host, strict_slashes=future.strict_slashes,
strict_slashes=future.strict_slashes name=future.name,
)(future.handler) )(future.handler)
# Middleware # Middleware
for future in self.middlewares: for future in self.middlewares:
@ -94,27 +100,35 @@ class Blueprint:
app.listener(event)(listener) app.listener(event)(listener)
def route(self, uri, methods=frozenset({'GET'}), host=None, def route(self, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False, stream=False, version=None): strict_slashes=None, stream=False, version=None, name=None):
"""Create a blueprint route from a decorated function. """Create a blueprint route from a decorated function.
:param uri: endpoint at which the route will be accessible. :param uri: endpoint at which the route will be accessible.
:param methods: list of acceptable HTTP methods. :param methods: list of acceptable HTTP methods.
""" """
if strict_slashes is None:
strict_slashes = self.strict_slashes
def decorator(handler): def decorator(handler):
route = FutureRoute( route = FutureRoute(
handler, uri, methods, host, strict_slashes, stream, version) handler, uri, methods, host, strict_slashes, stream, version,
name)
self.routes.append(route) self.routes.append(route)
return handler return handler
return decorator return decorator
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False, version=None): strict_slashes=None, version=None, name=None):
"""Create a blueprint route from a function. """Create a blueprint route from a function.
:param handler: function for handling uri requests. Accepts function, :param handler: function for handling uri requests. Accepts function,
or class instance with a view_class method. or class instance with a view_class method.
:param uri: endpoint at which the route will be accessible. :param uri: endpoint at which the route will be accessible.
:param methods: list of acceptable HTTP methods. :param methods: list of acceptable HTTP methods.
:param host:
:param strict_slashes:
:param version:
:param name: user defined route name for url_for
:return: function or class instance :return: function or class instance
""" """
# Handle HTTPMethodView differently # Handle HTTPMethodView differently
@ -125,27 +139,36 @@ class Blueprint:
if getattr(handler.view_class, method.lower(), None): if getattr(handler.view_class, method.lower(), None):
methods.add(method) methods.add(method)
if strict_slashes is None:
strict_slashes = self.strict_slashes
# handle composition view differently # handle composition view differently
if isinstance(handler, CompositionView): if isinstance(handler, CompositionView):
methods = handler.handlers.keys() methods = handler.handlers.keys()
self.route(uri=uri, methods=methods, host=host, self.route(uri=uri, methods=methods, host=host,
strict_slashes=strict_slashes, version=version)(handler) strict_slashes=strict_slashes, version=version,
name=name)(handler)
return handler return handler
def websocket(self, uri, host=None, strict_slashes=False, version=None): def websocket(self, uri, host=None, strict_slashes=None, version=None,
name=None):
"""Create a blueprint websocket route from a decorated function. """Create a blueprint websocket route from a decorated function.
:param uri: endpoint at which the route will be accessible. :param uri: endpoint at which the route will be accessible.
""" """
if strict_slashes is None:
strict_slashes = self.strict_slashes
def decorator(handler): def decorator(handler):
route = FutureRoute(handler, uri, [], host, strict_slashes, route = FutureRoute(handler, uri, [], host, strict_slashes,
False, version) False, version, name)
self.websocket_routes.append(route) self.websocket_routes.append(route)
return handler return handler
return decorator return decorator
def add_websocket_route(self, handler, uri, host=None, version=None): def add_websocket_route(self, handler, uri, host=None, version=None,
name=None):
"""Create a blueprint websocket route from a function. """Create a blueprint websocket route from a function.
:param handler: function for handling uri requests. Accepts function, :param handler: function for handling uri requests. Accepts function,
@ -153,7 +176,7 @@ class Blueprint:
:param uri: endpoint at which the route will be accessible. :param uri: endpoint at which the route will be accessible.
:return: function or class instance :return: function or class instance
""" """
self.websocket(uri=uri, host=host, version=version)(handler) self.websocket(uri=uri, host=host, version=version, name=name)(handler)
return handler return handler
def listener(self, event): def listener(self, event):
@ -195,40 +218,57 @@ 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)
strict_slashes = kwargs.get('strict_slashes')
if strict_slashes is None and self.strict_slashes is not None:
kwargs.update(strict_slashes=self.strict_slashes)
static = FutureStatic(uri, file_or_directory, args, kwargs) static = FutureStatic(uri, file_or_directory, args, kwargs)
self.statics.append(static) self.statics.append(static)
# Shorthand method decorators # Shorthand method decorators
def get(self, uri, host=None, strict_slashes=False, version=None): def get(self, uri, host=None, strict_slashes=None, version=None,
name=None):
return self.route(uri, methods=["GET"], host=host, return self.route(uri, methods=["GET"], host=host,
strict_slashes=strict_slashes, version=version) strict_slashes=strict_slashes, version=version,
name=name)
def post(self, uri, host=None, strict_slashes=False, stream=False, def post(self, uri, host=None, strict_slashes=None, stream=False,
version=None): version=None, name=None):
return self.route(uri, methods=["POST"], host=host, return self.route(uri, methods=["POST"], host=host,
strict_slashes=strict_slashes, stream=stream, strict_slashes=strict_slashes, stream=stream,
version=version) version=version, name=name)
def put(self, uri, host=None, strict_slashes=False, stream=False, def put(self, uri, host=None, strict_slashes=None, stream=False,
version=None): version=None, name=None):
return self.route(uri, methods=["PUT"], host=host, return self.route(uri, methods=["PUT"], host=host,
strict_slashes=strict_slashes, stream=stream, strict_slashes=strict_slashes, stream=stream,
version=version) version=version, name=name)
def head(self, uri, host=None, strict_slashes=False, version=None): def head(self, uri, host=None, strict_slashes=None, version=None,
name=None):
return self.route(uri, methods=["HEAD"], host=host, return self.route(uri, methods=["HEAD"], host=host,
strict_slashes=strict_slashes, version=version) strict_slashes=strict_slashes, version=version,
name=name)
def options(self, uri, host=None, strict_slashes=False, version=None): def options(self, uri, host=None, strict_slashes=None, version=None,
name=None):
return self.route(uri, methods=["OPTIONS"], host=host, return self.route(uri, methods=["OPTIONS"], host=host,
strict_slashes=strict_slashes, version=version) strict_slashes=strict_slashes, version=version,
name=name)
def patch(self, uri, host=None, strict_slashes=False, stream=False, def patch(self, uri, host=None, strict_slashes=None, stream=False,
version=None): version=None, name=None):
return self.route(uri, methods=["PATCH"], host=host, return self.route(uri, methods=["PATCH"], host=host,
strict_slashes=strict_slashes, stream=stream, strict_slashes=strict_slashes, stream=stream,
version=version) version=version, name=name)
def delete(self, uri, host=None, strict_slashes=False, version=None): def delete(self, uri, host=None, strict_slashes=None, version=None,
name=None):
return self.route(uri, methods=["DELETE"], host=host, return self.route(uri, methods=["DELETE"], host=host,
strict_slashes=strict_slashes, version=version) strict_slashes=strict_slashes, version=version,
name=name)

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': 'DEBUG',
'handlers': ['internal', 'errorStream']
},
'network': {
'level': 'DEBUG',
'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,13 +31,16 @@ 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
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
if load_env: if load_env:
self.load_environment_vars() prefix = SANIC_PREFIX if load_env is True else load_env
self.load_environment_vars(prefix=prefix)
def __getattr__(self, attr): def __getattr__(self, attr):
try: try:
@ -195,14 +104,14 @@ class Config(dict):
if key.isupper(): if key.isupper():
self[key] = getattr(obj, key) self[key] = getattr(obj, key)
def load_environment_vars(self): def load_environment_vars(self, prefix=SANIC_PREFIX):
""" """
Looks for any ``SANIC_`` prefixed environment variables and applies Looks for prefixed environment variables and applies
them to the configuration if present. them to the configuration if present.
""" """
for k, v in os.environ.items(): for k, v in os.environ.items():
if k.startswith(SANIC_PREFIX): if k.startswith(prefix):
_, config_key = k.split(SANIC_PREFIX, 1) _, config_key = k.split(prefix, 1)
try: try:
self[config_key] = int(v) self[config_key] = int(v)
except ValueError: except ValueError:

View File

@ -98,7 +98,8 @@ 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")
return super().__setitem__(key, value) if value is not False:
return super().__setitem__(key, value)
def encode(self, encoding): def encode(self, encoding):
output = ['%s=%s' % (self.key, _quote(self.value))] output = ['%s=%s' % (self.key, _quote(self.value))]

View File

@ -1,4 +1,4 @@
from sanic.response import ALL_STATUS_CODES, COMMON_STATUS_CODES from sanic.response import STATUS_CODES
TRACEBACK_STYLE = ''' TRACEBACK_STYLE = '''
<style> <style>
@ -150,18 +150,33 @@ class InvalidUsage(SanicException):
pass pass
@add_status_code(405)
class MethodNotSupported(SanicException):
def __init__(self, message, method, allowed_methods):
super().__init__(message)
self.headers = dict()
self.headers["Allow"] = ", ".join(allowed_methods)
if method in ['HEAD', 'PATCH', 'PUT', 'DELETE']:
self.headers['Content-Length'] = 0
@add_status_code(500) @add_status_code(500)
class ServerError(SanicException): class ServerError(SanicException):
pass pass
@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
class FileNotFound(NotFound): class FileNotFound(NotFound):
pass
def __init__(self, message, path, relative_url): def __init__(self, message, path, relative_url):
super().__init__(message) super().__init__(message)
self.path = path self.path = path
@ -170,6 +185,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
@ -184,8 +206,6 @@ class HeaderNotFound(InvalidUsage):
@add_status_code(416) @add_status_code(416)
class ContentRangeError(SanicException): class ContentRangeError(SanicException):
pass
def __init__(self, message, content_range): def __init__(self, message, content_range):
super().__init__(message) super().__init__(message)
self.headers = { self.headers = {
@ -208,45 +228,47 @@ class Unauthorized(SanicException):
""" """
Unauthorized exception (401 HTTP status code). Unauthorized exception (401 HTTP status code).
:param message: Message describing the exception.
:param status_code: HTTP Status code.
:param scheme: Name of the authentication scheme to be used. :param scheme: Name of the authentication scheme to be used.
:param challenge: A dict containing values to add to the WWW-Authenticate
header that is generated. This is especially useful when dealing with When present, kwargs is used to complete the WWW-Authentication header.
the Digest scheme. (optional)
Examples:: Examples::
# With a Basic auth-scheme, realm MUST be present: # With a Basic auth-scheme, realm MUST be present:
challenge = {"realm": "Restricted Area"} raise Unauthorized("Auth required.",
raise Unauthorized("Auth required.", "Basic", challenge) scheme="Basic",
realm="Restricted Area")
# With a Digest auth-scheme, things are a bit more complicated: # With a Digest auth-scheme, things are a bit more complicated:
challenge = { raise Unauthorized("Auth required.",
"realm": "Restricted Area", scheme="Digest",
"qop": "auth, auth-int", realm="Restricted Area",
"algorithm": "MD5", qop="auth, auth-int",
"nonce": "abcdef", algorithm="MD5",
"opaque": "zyxwvu" nonce="abcdef",
} opaque="zyxwvu")
raise Unauthorized("Auth required.", "Digest", challenge)
# With a Bearer auth-scheme, realm is optional: # With a Bearer auth-scheme, realm is optional so you can write:
challenge = {"realm": "Restricted Area"} raise Unauthorized("Auth required.", scheme="Bearer")
raise Unauthorized("Auth required.", "Bearer", challenge)
# or, if you want to specify the realm:
raise Unauthorized("Auth required.",
scheme="Bearer",
realm="Restricted Area")
""" """
pass def __init__(self, message, status_code=None, scheme=None, **kwargs):
super().__init__(message, status_code)
def __init__(self, message, scheme, challenge=None): # if auth-scheme is specified, set "WWW-Authenticate" header
super().__init__(message) if scheme is not None:
values = ["{!s}={!r}".format(k, v) for k, v in kwargs.items()]
challenge = ', '.join(values)
chal = "" self.headers = {
"WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip()
if challenge is not None: }
values = ["{!s}={!r}".format(k, v) for k, v in challenge.items()]
chal = ', '.join(values)
self.headers = {
"WWW-Authenticate": "{} {}".format(scheme, chal).rstrip()
}
def abort(status_code, message=None): def abort(status_code, message=None):
@ -259,8 +281,7 @@ def abort(status_code, message=None):
in response.py for the given status code. in response.py for the given status code.
""" """
if message is None: if message is None:
message = COMMON_STATUS_CODES.get(status_code, message = STATUS_CODES.get(status_code)
ALL_STATUS_CODES.get(status_code))
# These are stored as bytes in the STATUS_CODES dict # These are stored as bytes in the STATUS_CODES dict
message = message.decode('utf8') message = message.decode('utf8')
sanic_exception = _sanic_exceptions.get(status_code, SanicException) sanic_exception = _sanic_exceptions.get(status_code, SanicException)

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

@ -1,5 +1,6 @@
import sys import sys
import json import json
import socket
from cgi import parse_header from cgi import parse_header
from collections import namedtuple from collections import namedtuple
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
@ -17,10 +18,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"
@ -45,7 +47,8 @@ class Request(dict):
__slots__ = ( __slots__ = (
'app', 'headers', 'version', 'method', '_cookies', 'transport', 'app', 'headers', 'version', 'method', '_cookies', 'transport',
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', 'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
'_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr' '_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr',
'_socket', '_port'
) )
def __init__(self, url_bytes, headers, version, method, transport): def __init__(self, url_bytes, headers, version, method, transport):
@ -68,15 +71,27 @@ 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:
try: self.load_json()
self.parsed_json = json_loads(self.body)
except Exception: return self.parsed_json
if not self.body:
return None def load_json(self, loads=json_loads):
raise InvalidUsage("Failed when parsing body as json") try:
self.parsed_json = loads(self.body)
except Exception:
if not self.body:
return None
raise InvalidUsage("Failed when parsing body as json")
return self.parsed_json return self.parsed_json
@ -114,7 +129,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
@ -154,11 +169,36 @@ class Request(dict):
@property @property
def ip(self): def ip(self):
if not hasattr(self, '_ip'): if not hasattr(self, '_socket'):
self._ip = (self.transport.get_extra_info('peername') or self._get_address()
(None, None))
return self._ip return self._ip
@property
def port(self):
if not hasattr(self, '_socket'):
self._get_address()
return self._port
@property
def socket(self):
if not hasattr(self, '_socket'):
self._get_socket()
return self._socket
def _get_address(self):
sock = self.transport.get_extra_info('socket')
if sock.family == socket.AF_INET:
self._socket = (self.transport.get_extra_info('peername') or
(None, None))
self._ip, self._port = self._socket
elif sock.family == socket.AF_INET6:
self._socket = (self.transport.get_extra_info('peername') or
(None, None, None, None))
self._ip, self._port, *_ = self._socket
else:
self._ip, self._port = (None, None)
@property @property
def remote_addr(self): def remote_addr(self):
"""Attempt to return the original client ip based on X-Forwarded-For. """Attempt to return the original client ip based on X-Forwarded-For.
@ -170,8 +210,8 @@ class Request(dict):
remote_addrs = [ remote_addrs = [
addr for addr in [ addr for addr in [
addr.strip() for addr in forwarded_for addr.strip() for addr in forwarded_for
] if addr ] if addr
] ]
if len(remote_addrs) > 0: if len(remote_addrs) > 0:
self._remote_addr = remote_addrs[0] self._remote_addr = remote_addrs[0]
else: else:

View File

@ -3,20 +3,14 @@ from os import path
try: try:
from ujson import dumps as json_dumps from ujson import dumps as json_dumps
except: except BaseException:
from json import dumps as json_dumps from json import dumps as json_dumps
from aiofiles import open as open_async from aiofiles import open as open_async
from sanic.cookies import CookieJar from sanic.cookies import CookieJar
COMMON_STATUS_CODES = { STATUS_CODES = {
200: b'OK',
400: b'Bad Request',
404: b'Not Found',
500: b'Internal Server Error',
}
ALL_STATUS_CODES = {
100: b'Continue', 100: b'Continue',
101: b'Switching Protocols', 101: b'Switching Protocols',
102: b'Processing', 102: b'Processing',
@ -56,6 +50,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 +58,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',
@ -109,8 +105,9 @@ class BaseHTTPResponse:
class StreamingHTTPResponse(BaseHTTPResponse): class StreamingHTTPResponse(BaseHTTPResponse):
__slots__ = ( __slots__ = (
'transport', 'streaming_fn', 'transport', 'streaming_fn', 'status',
'status', 'content_type', 'headers', '_cookies') 'content_type', 'headers', '_cookies'
)
def __init__(self, streaming_fn, status=200, headers=None, def __init__(self, streaming_fn, status=200, headers=None,
content_type='text/plain'): content_type='text/plain'):
@ -159,11 +156,10 @@ class StreamingHTTPResponse(BaseHTTPResponse):
headers = self._parse_headers() headers = self._parse_headers()
# Try to pull from the common codes first if self.status is 200:
# Speeds up response rate 6% over pulling from all status = b'OK'
status = COMMON_STATUS_CODES.get(self.status) else:
if not status: status = STATUS_CODES.get(self.status)
status = ALL_STATUS_CODES.get(self.status)
return (b'HTTP/%b %d %b\r\n' return (b'HTTP/%b %d %b\r\n'
b'%b' b'%b'
@ -206,11 +202,10 @@ class HTTPResponse(BaseHTTPResponse):
headers = self._parse_headers() headers = self._parse_headers()
# Try to pull from the common codes first if self.status is 200:
# Speeds up response rate 6% over pulling from all status = b'OK'
status = COMMON_STATUS_CODES.get(self.status) else:
if not status: status = STATUS_CODES.get(self.status, b'UNKNOWN RESPONSE')
status = ALL_STATUS_CODES.get(self.status, b'UNKNOWN RESPONSE')
return (b'HTTP/%b %d %b\r\n' return (b'HTTP/%b %d %b\r\n'
b'Connection: %b\r\n' b'Connection: %b\r\n'
@ -234,7 +229,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.
@ -243,7 +239,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)
@ -288,15 +284,22 @@ def html(body, status=200, headers=None):
content_type="text/html; charset=utf-8") content_type="text/html; charset=utf-8")
async def file(location, mime_type=None, headers=None, _range=None): async def file(
location, mime_type=None, headers=None, filename=None, _range=None):
"""Return a response object with file data. """Return a response object with file data.
:param location: Location of file on system. :param location: Location of file on system.
:param mime_type: Specific mime_type. :param mime_type: Specific mime_type.
:param headers: Custom Headers. :param headers: Custom Headers.
:param filename: Override filename.
:param _range: :param _range:
""" """
filename = path.split(location)[-1] headers = headers or {}
if filename:
headers.setdefault(
'Content-Disposition',
'attachment; filename="{}"'.format(filename))
filename = filename or path.split(location)[-1]
async with open_async(location, mode='rb') as _file: async with open_async(location, mode='rb') as _file:
if _range: if _range:
@ -308,24 +311,30 @@ async def file(location, mime_type=None, headers=None, _range=None):
out_stream = await _file.read() out_stream = await _file.read()
mime_type = mime_type or guess_type(filename)[0] or 'text/plain' mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
return HTTPResponse(status=200, return HTTPResponse(status=200,
headers=headers, headers=headers,
content_type=mime_type, content_type=mime_type,
body_bytes=out_stream) body_bytes=out_stream)
async def file_stream(location, chunk_size=4096, mime_type=None, headers=None, async def file_stream(
_range=None): location, chunk_size=4096, mime_type=None, headers=None,
filename=None, _range=None):
"""Return a streaming response object with file data. """Return a streaming response object with file data.
:param location: Location of file on system. :param location: Location of file on system.
:param chunk_size: The size of each chunk in the stream (in bytes) :param chunk_size: The size of each chunk in the stream (in bytes)
:param mime_type: Specific mime_type. :param mime_type: Specific mime_type.
:param headers: Custom Headers. :param headers: Custom Headers.
:param filename: Override filename.
:param _range: :param _range:
""" """
filename = path.split(location)[-1] headers = headers or {}
if filename:
headers.setdefault(
'Content-Disposition',
'attachment; filename="{}"'.format(filename))
filename = filename or path.split(location)[-1]
_file = await open_async(location, mode='rb') _file = await open_async(location, mode='rb')

View File

@ -3,7 +3,7 @@ from collections import defaultdict, namedtuple
from collections.abc import Iterable from collections.abc import Iterable
from functools import lru_cache from functools import lru_cache
from sanic.exceptions import NotFound, InvalidUsage from sanic.exceptions import NotFound, MethodNotSupported
from sanic.views import CompositionView from sanic.views import CompositionView
Route = namedtuple( Route = namedtuple(
@ -67,6 +67,8 @@ class Router:
def __init__(self): def __init__(self):
self.routes_all = {} self.routes_all = {}
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 = []
@ -91,6 +93,10 @@ class Router:
pattern = 'string' pattern = 'string'
if ':' in parameter_string: if ':' in parameter_string:
name, pattern = parameter_string.split(':', 1) name, pattern = parameter_string.split(':', 1)
if not name:
raise ValueError(
"Invalid parameter syntax: {}".format(parameter_string)
)
default = (str, pattern) default = (str, pattern)
# Pull from pre-configured types # Pull from pre-configured types
@ -99,7 +105,7 @@ class Router:
return name, _type, pattern return name, _type, pattern
def add(self, uri, methods, handler, host=None, strict_slashes=False, def add(self, uri, methods, handler, host=None, strict_slashes=False,
version=None): version=None, name=None):
"""Add a handler to the route list """Add a handler to the route list
:param uri: path to match :param uri: path to match
@ -113,34 +119,38 @@ class Router:
:return: Nothing :return: Nothing
""" """
if version is not None: if version is not None:
if uri.startswith('/'): version = re.escape(str(version).strip('/').lstrip('v'))
uri = "/".join(["/v{}".format(str(version)), uri[1:]]) uri = "/".join(["/v{}".format(version), uri.lstrip('/')])
else:
uri = "/".join(["/v{}".format(str(version)), uri])
# add regular version # add regular version
self._add(uri, methods, handler, host) self._add(uri, methods, handler, host, name)
if strict_slashes: if strict_slashes:
return return
# Add versions with and without trailing / # Add versions with and without trailing /
slashed_methods = self.routes_all.get(uri + '/', frozenset({}))
if isinstance(methods, Iterable):
_slash_is_missing = all(method in slashed_methods for
method in methods)
else:
_slash_is_missing = methods in slashed_methods
slash_is_missing = ( slash_is_missing = (
not uri[-1] == '/' not uri[-1] == '/' and not _slash_is_missing
and not self.routes_all.get(uri + '/', False)
) )
without_slash_is_missing = ( without_slash_is_missing = (
uri[-1] == '/' uri[-1] == '/' and not
and not self.routes_all.get(uri[:-1], False) self.routes_all.get(uri[:-1], False) and not
and not uri == '/' uri == '/'
) )
# add version with trailing slash # add version with trailing slash
if slash_is_missing: if slash_is_missing:
self._add(uri + '/', methods, handler, host) self._add(uri + '/', methods, handler, host, name)
# add version without trailing slash # add version without trailing slash
elif without_slash_is_missing: elif without_slash_is_missing:
self._add(uri[:-1], methods, handler, host) self._add(uri[:-1], methods, handler, host, name)
def _add(self, uri, methods, handler, host=None): def _add(self, uri, methods, handler, host=None, name=None):
"""Add a handler to the route list """Add a handler to the route list
:param uri: path to match :param uri: path to match
@ -148,6 +158,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:
@ -161,7 +172,7 @@ class Router:
"host strings, not {!r}".format(host)) "host strings, not {!r}".format(host))
for host_ in host: for host_ in host:
self.add(uri, methods, handler, host_) self.add(uri, methods, handler, host_, name)
return return
# Dict for faster lookups of if method allowed # Dict for faster lookups of if method allowed
@ -229,22 +240,38 @@ class Router:
else: else:
route = self.routes_all.get(uri) route = self.routes_all.get(uri)
# prefix the handler name with the blueprint name
# 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__'):
handler_name = '{}.{}'.format(
handler.__blueprintname__, name or handler.__name__)
else:
handler_name = name or getattr(handler, '__name__', None)
if route: if route:
route = merge_route(route, methods, handler) route = merge_route(route, methods, handler)
else: else:
# prefix the handler name with the blueprint name
# if available
if hasattr(handler, '__blueprintname__'):
handler_name = '{}.{}'.format(
handler.__blueprintname__, handler.__name__)
else:
handler_name = getattr(handler, '__name__', None)
route = Route( route = Route(
handler=handler, methods=methods, pattern=pattern, handler=handler, methods=methods, pattern=pattern,
parameters=parameters, name=handler_name, uri=uri) parameters=parameters, name=handler_name, uri=uri)
self.routes_all[uri] = route self.routes_all[uri] = route
if is_static:
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)
if properties['unhashable']: if properties['unhashable']:
self.routes_always_check.append(route) self.routes_always_check.append(route)
elif parameters: elif parameters:
@ -265,6 +292,16 @@ class Router:
uri = host + uri uri = host + uri
try: try:
route = self.routes_all.pop(uri) route = self.routes_all.pop(uri)
for handler_name, pairs in self.routes_names.items():
if pairs[0] == uri:
self.routes_names.pop(handler_name)
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))
@ -280,20 +317,20 @@ 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)
for uri, route in self.routes_all.items(): if view_name == 'static' or view_name.endswith('.static'):
if route.name == view_name: return self.routes_static_files.get(name, (None, None))
return uri, route
return (None, None) return self.routes_names.get(view_name, (None, None))
def get(self, request): def get(self, request):
"""Get a request handler based on the URL of the request, or raises an """Get a request handler based on the URL of the request, or raises an
@ -313,6 +350,16 @@ class Router:
except NotFound: except NotFound:
return self._get(request.path, request.method, '') return self._get(request.path, request.method, '')
def get_supported_methods(self, url):
"""Get a list of supported methods for a url and optional host.
:param url: URL string (including host)
:return: frozenset of supported methods
"""
route = self.routes_all.get(url)
# if methods are None then this logic will prevent an error
return getattr(route, 'methods', None) or frozenset()
@lru_cache(maxsize=ROUTER_CACHE_SIZE) @lru_cache(maxsize=ROUTER_CACHE_SIZE)
def _get(self, url, method, host): def _get(self, url, method, host):
"""Get a request handler based on the URL of the request, or raises an """Get a request handler based on the URL of the request, or raises an
@ -325,9 +372,10 @@ class Router:
url = host + url url = host + url
# Check against known static routes # Check against known static routes
route = self.routes_static.get(url) route = self.routes_static.get(url)
method_not_supported = InvalidUsage( method_not_supported = MethodNotSupported(
'Method {} not allowed for URL {}'.format( 'Method {} not allowed for URL {}'.format(method, url),
method, url), status_code=405) method=method,
allowed_methods=self.get_supported_methods(url))
if route: if route:
if route.methods and method not in route.methods: if route.methods and method not in route.methods:
raise method_not_supported raise method_not_supported
@ -370,7 +418,7 @@ class Router:
""" """
try: try:
handler = self.get(request)[0] handler = self.get(request)[0]
except (NotFound, InvalidUsage): except (NotFound, MethodNotSupported):
return False return False
if (hasattr(handler, 'view_class') and if (hasattr(handler, 'view_class') and
hasattr(handler.view_class, request.method.lower())): hasattr(handler.view_class, request.method.lower())):

View File

@ -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=5,
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,41 @@ 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:
if self._request_stream_task:
self._request_stream_task.cancel()
if self._request_handler_task:
self._request_handler_task.cancel()
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()
self.transport = None
# -------------------------------------------- # # -------------------------------------------- #
# Parsing # Parsing
# -------------------------------------------- # # -------------------------------------------- #
@ -189,10 +243,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 +260,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 +280,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 +293,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 +305,53 @@ 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
extra['host'] = 'UNKNOWN'
if self.request is not None:
if self.request.ip:
extra['host'] = '{0[0]}:{0[1]}'.format(self.request.ip)
extra['request'] = '{0} {1}'.format(self.request.method,
self.request.url)
else:
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( if self._debug:
'Connection lost before response written @ {}'.format( logger.error('Connection lost before response written @ %s',
self.request.ip)) self.request.ip)
keep_alive = False
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(
@ -272,8 +359,12 @@ class HttpProtocol(asyncio.Protocol):
finally: finally:
if not keep_alive: if not keep_alive:
self.transport.close() self.transport.close()
self.transport = None
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 +373,25 @@ 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( if self._debug:
'Connection lost before response written @ {}'.format( logger.error('Connection lost before response written @ %s',
self.request.ip)) self.request.ip)
keep_alive = False
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(
@ -314,59 +399,55 @@ class HttpProtocol(asyncio.Protocol):
finally: finally:
if not keep_alive: if not keep_alive:
self.transport.close() self.transport.close()
self.transport = None
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( if self._debug:
'Connection lost before error written @ {}'.format( logger.error('Connection lost before error written @ %s',
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 +502,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=5,
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.
@ -446,6 +528,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
`app` instance and `loop` `app` instance and `loop`
:param debug: enables debug output (slows server) :param debug: enables debug output (slows server)
:param request_timeout: time in seconds :param request_timeout: time in seconds
:param response_timeout: time in seconds
:param keep_alive_timeout: time in seconds
:param ssl: SSLContext :param ssl: SSLContext
:param sock: Socket for the server to accept connections from :param sock: Socket for the server to accept connections from
:param request_max_size: size in bytes, `None` for no limit :param request_max_size: size in bytes, `None` for no limit
@ -453,7 +537,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 +558,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,
@ -507,8 +593,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
try: try:
http_server = loop.run_until_complete(server_coroutine) http_server = loop.run_until_complete(server_coroutine)
except: except BaseException:
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 +605,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)
@ -554,7 +640,9 @@ def serve(host, port, request_handler, error_handler, before_start=None,
coros = [] coros = []
for conn in connections: for conn in connections:
if hasattr(conn, "websocket") and conn.websocket: if hasattr(conn, "websocket") and conn.websocket:
coros.append(conn.websocket.close_connection(force=True)) coros.append(
conn.websocket.close_connection(after_handshake=True)
)
else: else:
conn.close() conn.close()
@ -588,8 +676,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,8 @@ from sanic.response import file, file_stream, HTTPResponse
def register(app, uri, file_or_directory, pattern, def register(app, uri, file_or_directory, pattern,
use_modified_since, use_content_range, use_modified_since, use_content_range,
stream_large_files): stream_large_files, name='static', host=None,
strict_slashes=None):
# TODO: Though sanic is not a file server, I feel like we should at least # TODO: Though sanic is not a file server, I feel like we should at least
# make a good effort here. Modified-since is nice, but we could # make a good effort here. Modified-since is nice, but we could
# also look into etags, expires, and caching # also look into etags, expires, and caching
@ -39,6 +40,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
@ -102,7 +104,7 @@ def register(app, uri, file_or_directory, pattern,
if isinstance(stream_large_files, int): if isinstance(stream_large_files, int):
threshold = stream_large_files threshold = stream_large_files
else: else:
threshold = 1024*1000 threshold = 1024 * 1024
if not stats: if not stats:
stats = await stat(file_path) stats = await stat(file_path)
@ -117,4 +119,9 @@ 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,
strict_slashes=strict_slashes)(_handler)

View File

@ -1,15 +1,16 @@
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
class SanicTestClient: class SanicTestClient:
def __init__(self, app): def __init__(self, app, port=PORT):
self.app = app self.app = app
self.port = port
async def _local_request(self, method, uri, cookies=None, *args, **kwargs): async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
import aiohttp import aiohttp
@ -17,9 +18,9 @@ class SanicTestClient:
url = uri url = uri
else: else:
url = 'http://{host}:{port}{uri}'.format( url = 'http://{host}:{port}{uri}'.format(
host=HOST, port=PORT, uri=uri) host=HOST, port=self.port, uri=uri)
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,12 +62,12 @@ 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()
self.app.run(host=HOST, debug=debug, port=PORT, **server_kwargs) self.app.run(host=HOST, debug=debug, port=self.port, **server_kwargs)
self.app.listeners['after_server_start'].pop() self.app.listeners['after_server_start'].pop()
if exceptions: if exceptions:
@ -76,14 +77,14 @@ class SanicTestClient:
try: try:
request, response = results request, response = results
return request, response return request, response
except: except BaseException:
raise ValueError( raise ValueError(
"Request and response object expected, got ({})".format( "Request and response object expected, got ({})".format(
results)) results))
else: else:
try: try:
return results[-1] return results[-1]
except: except BaseException:
raise ValueError( raise ValueError(
"Request object expected, got ({})".format(results)) "Request object expected, got ({})".format(results))

View File

@ -13,10 +13,18 @@ class WebSocketProtocol(HttpProtocol):
self.websocket_max_size = websocket_max_size self.websocket_max_size = websocket_max_size
self.websocket_max_queue = websocket_max_queue self.websocket_max_queue = websocket_max_queue
def connection_timeout(self): # timeouts make no sense for websocket routes
# timeouts make no sense for websocket routes def request_timeout_callback(self):
if self.websocket is None: if self.websocket is None:
super().connection_timeout() super().request_timeout_callback()
def response_timeout_callback(self):
if self.websocket is None:
super().response_timeout_callback()
def keep_alive_timeout_callback(self):
if self.websocket is None:
super().keep_alive_timeout_callback()
def connection_lost(self, exc): def connection_lost(self, exc):
if self.websocket is not None: if self.websocket is not None:
@ -41,7 +49,7 @@ class WebSocketProtocol(HttpProtocol):
else: else:
super().write_response(response) super().write_response(response)
async def websocket_handshake(self, request): async def websocket_handshake(self, request, subprotocols=None):
# let the websockets package do the handshake with the client # let the websockets package do the handshake with the client
headers = [] headers = []
@ -57,6 +65,17 @@ class WebSocketProtocol(HttpProtocol):
except InvalidHandshake: except InvalidHandshake:
raise InvalidUsage('Invalid websocket request') raise InvalidUsage('Invalid websocket request')
subprotocol = None
if subprotocols and 'Sec-Websocket-Protocol' in request.headers:
# select a subprotocol
client_subprotocols = [p.strip() for p in request.headers[
'Sec-Websocket-Protocol'].split(',')]
for p in client_subprotocols:
if p in subprotocols:
subprotocol = p
set_header('Sec-Websocket-Protocol', subprotocol)
break
# write the 101 response back to the client # write the 101 response back to the client
rv = b'HTTP/1.1 101 Switching Protocols\r\n' rv = b'HTTP/1.1 101 Switching Protocols\r\n'
for k, v in headers: for k, v in headers:
@ -69,5 +88,7 @@ class WebSocketProtocol(HttpProtocol):
max_size=self.websocket_max_size, max_size=self.websocket_max_size,
max_queue=self.websocket_max_queue max_queue=self.websocket_max_queue
) )
self.websocket.subprotocol = subprotocol
self.websocket.connection_made(request.transport) self.websocket.connection_made(request.transport)
self.websocket.connection_open()
return self.websocket return self.websocket

View File

@ -23,6 +23,9 @@ from sanic.websocket import WebSocketProtocol
class GunicornWorker(base.Worker): class GunicornWorker(base.Worker):
http_protocol = HttpProtocol
websocket_protocol = WebSocketProtocol
def __init__(self, *args, **kw): # pragma: no cover def __init__(self, *args, **kw): # pragma: no cover
super().__init__(*args, **kw) super().__init__(*args, **kw)
cfg = self.cfg cfg = self.cfg
@ -46,8 +49,9 @@ class GunicornWorker(base.Worker):
def run(self): def run(self):
is_debug = self.log.loglevel == logging.DEBUG is_debug = self.log.loglevel == logging.DEBUG
protocol = (WebSocketProtocol if self.app.callable.websocket_enabled protocol = (
else HttpProtocol) self.websocket_protocol if self.app.callable.websocket_enabled
else self.http_protocol)
self._server_settings = self.app.callable._helper( self._server_settings = self.app.callable._helper(
loop=self.loop, loop=self.loop,
debug=is_debug, debug=is_debug,
@ -70,13 +74,13 @@ class GunicornWorker(base.Worker):
trigger_events(self._server_settings.get('before_stop', []), trigger_events(self._server_settings.get('before_stop', []),
self.loop) self.loop)
self.loop.run_until_complete(self.close()) self.loop.run_until_complete(self.close())
except: except BaseException:
traceback.print_exc() traceback.print_exc()
finally: finally:
try: try:
trigger_events(self._server_settings.get('after_stop', []), trigger_events(self._server_settings.get('after_stop', []),
self.loop) self.loop)
except: except BaseException:
traceback.print_exc() traceback.print_exc()
finally: finally:
self.loop.close() self.loop.close()
@ -111,7 +115,9 @@ class GunicornWorker(base.Worker):
coros = [] coros = []
for conn in self.connections: for conn in self.connections:
if hasattr(conn, "websocket") and conn.websocket: if hasattr(conn, "websocket") and conn.websocket:
coros.append(conn.websocket.close_connection(force=True)) coros.append(
conn.websocket.close_connection(after_handshake=False)
)
else: else:
conn.close() conn.close()
_shutdown = asyncio.gather(*coros, loop=self.loop) _shutdown = asyncio.gather(*coros, loop=self.loop)
@ -142,9 +148,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

@ -59,7 +59,7 @@ requirements = [
uvloop, uvloop,
ujson, ujson,
'aiofiles>=0.3.0', 'aiofiles>=0.3.0',
'websockets>=3.2', 'websockets>=4.0',
] ]
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")): if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
print("Installing without uJSON") print("Installing without uJSON")

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

@ -78,6 +78,65 @@ def test_bp_strict_slash():
request, response = app.test_client.post('/post') request, response = app.test_client.post('/post')
assert response.status == 404 assert response.status == 404
def test_bp_strict_slash_default_value():
app = Sanic('test_route_strict_slash')
bp = Blueprint('test_text', strict_slashes=True)
@bp.get('/get')
def handler(request):
return text('OK')
@bp.post('/post/')
def handler(request):
return text('OK')
app.blueprint(bp)
request, response = app.test_client.get('/get/')
assert response.status == 404
request, response = app.test_client.post('/post')
assert response.status == 404
def test_bp_strict_slash_without_passing_default_value():
app = Sanic('test_route_strict_slash')
bp = Blueprint('test_text')
@bp.get('/get')
def handler(request):
return text('OK')
@bp.post('/post/')
def handler(request):
return text('OK')
app.blueprint(bp)
request, response = app.test_client.get('/get/')
assert response.text == 'OK'
request, response = app.test_client.post('/post')
assert response.text == 'OK'
def test_bp_strict_slash_default_value_can_be_overwritten():
app = Sanic('test_route_strict_slash')
bp = Blueprint('test_text', strict_slashes=True)
@bp.get('/get', strict_slashes=False)
def handler(request):
return text('OK')
@bp.post('/post/', strict_slashes=False)
def handler(request):
return text('OK')
app.blueprint(bp)
request, response = app.test_client.get('/get/')
assert response.text == 'OK'
request, response = app.test_client.post('/post')
assert response.text == 'OK'
def test_bp_with_url_prefix(): def test_bp_with_url_prefix():
app = Sanic('test_text') app = Sanic('test_text')

View File

@ -19,15 +19,21 @@ def test_load_from_object():
def test_auto_load_env(): def test_auto_load_env():
environ["SANIC_TEST_ANSWER"] = "42" environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic() app = Sanic()
assert app.config.TEST_ANSWER == "42" assert app.config.TEST_ANSWER == 42
del environ["SANIC_TEST_ANSWER"] del environ["SANIC_TEST_ANSWER"]
def test_auto_load_env(): def test_dont_load_env():
environ["SANIC_TEST_ANSWER"] = "42" environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic(load_env=False) app = Sanic(load_env=False)
assert getattr(app.config, 'TEST_ANSWER', None) == None assert getattr(app.config, 'TEST_ANSWER', None) == None
del environ["SANIC_TEST_ANSWER"] del environ["SANIC_TEST_ANSWER"]
def test_load_env_prefix():
environ["MYAPP_TEST_ANSWER"] = "42"
app = Sanic(load_env='MYAPP_')
assert app.config.TEST_ANSWER == 42
del environ["MYAPP_TEST_ANSWER"]
def test_load_from_file(): def test_load_from_file():
app = Sanic('test_load_from_file') app = Sanic('test_load_from_file')
config = b""" config = b"""

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

@ -31,31 +31,38 @@ def exception_app():
def handler_403(request): def handler_403(request):
raise Forbidden("Forbidden") raise Forbidden("Forbidden")
@app.route('/401')
def handler_401(request):
raise Unauthorized("Unauthorized")
@app.route('/401/basic') @app.route('/401/basic')
def handler_401_basic(request): def handler_401_basic(request):
raise Unauthorized("Unauthorized", "Basic", {"realm": "Sanic"}) raise Unauthorized("Unauthorized", scheme="Basic", realm="Sanic")
@app.route('/401/digest') @app.route('/401/digest')
def handler_401_digest(request): def handler_401_digest(request):
challenge = { raise Unauthorized("Unauthorized",
"realm": "Sanic", scheme="Digest",
"qop": "auth, auth-int", realm="Sanic",
"algorithm": "MD5", qop="auth, auth-int",
"nonce": "abcdef", algorithm="MD5",
"opaque": "zyxwvu", nonce="abcdef",
} opaque="zyxwvu")
raise Unauthorized("Unauthorized", "Digest", challenge)
@app.route('/401/bearer') @app.route('/401/bearer')
def handler_401_bearer(request): def handler_401_bearer(request):
raise Unauthorized("Unauthorized", "Bearer") raise Unauthorized("Unauthorized", scheme="Bearer")
@app.route('/invalid') @app.route('/invalid')
def handler_invalid(request): def handler_invalid(request):
raise InvalidUsage("OK") raise InvalidUsage("OK")
@app.route('/abort/401')
def handler_401_error(request):
abort(401)
@app.route('/abort') @app.route('/abort')
def handler_invalid(request): def handler_500_error(request):
abort(500) abort(500)
return text("OK") return text("OK")
@ -122,9 +129,12 @@ def test_forbidden_exception(exception_app):
request, response = exception_app.test_client.get('/403') request, response = exception_app.test_client.get('/403')
assert response.status == 403 assert response.status == 403
def test_unauthorized_exception(exception_app): def test_unauthorized_exception(exception_app):
"""Test the built-in Unauthorized exception""" """Test the built-in Unauthorized exception"""
request, response = exception_app.test_client.get('/401')
assert response.status == 401
request, response = exception_app.test_client.get('/401/basic') request, response = exception_app.test_client.get('/401/basic')
assert response.status == 401 assert response.status == 401
assert response.headers.get('WWW-Authenticate') is not None assert response.headers.get('WWW-Authenticate') is not None
@ -132,7 +142,7 @@ def test_unauthorized_exception(exception_app):
request, response = exception_app.test_client.get('/401/digest') request, response = exception_app.test_client.get('/401/digest')
assert response.status == 401 assert response.status == 401
auth_header = response.headers.get('WWW-Authenticate') auth_header = response.headers.get('WWW-Authenticate')
assert auth_header is not None assert auth_header is not None
assert auth_header.startswith('Digest') assert auth_header.startswith('Digest')
@ -176,7 +186,7 @@ def test_exception_in_exception_handler_debug_off(exception_app):
assert response.body == b'An error occurred while handling an error' assert response.body == b'An error occurred while handling an error'
def test_exception_in_exception_handler_debug_off(exception_app): def test_exception_in_exception_handler_debug_on(exception_app):
"""Test that an exception thrown in an error handler is handled""" """Test that an exception thrown in an error handler is handled"""
request, response = exception_app.test_client.get( request, response = exception_app.test_client.get(
'/error_in_error_handler_handler', '/error_in_error_handler_handler',
@ -187,5 +197,8 @@ def test_exception_in_exception_handler_debug_off(exception_app):
def test_abort(exception_app): def test_abort(exception_app):
"""Test the abort function""" """Test the abort function"""
request, response = exception_app.test_client.get('/abort/401')
assert response.status == 401
request, response = exception_app.test_client.get('/abort') request, response = exception_app.test_client.get('/abort')
assert response.status == 500 assert response.status == 500

View File

@ -24,7 +24,7 @@ def handler_3(request):
@exception_handler_app.route('/4') @exception_handler_app.route('/4')
def handler_4(request): def handler_4(request):
foo = bar foo = bar # noqa -- F821 undefined name 'bar' is done to throw exception
return text(foo) return text(foo)

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,15 +1,28 @@
import asyncio
import uuid import uuid
from sanic.response import text
from sanic import Sanic
from io import StringIO
import logging import logging
from io import StringIO
from importlib import reload
import pytest
from unittest.mock import Mock
import sanic
from sanic.response import text
from sanic.log import LOGGING_CONFIG_DEFAULTS
from sanic import Sanic
logging_format = '''module: %(module)s; \ logging_format = '''module: %(module)s; \
function: %(funcName)s(); \ function: %(funcName)s(); \
message: %(message)s''' message: %(message)s'''
def reset_logging():
logging.shutdown()
reload(logging)
def test_log(): def test_log():
log_stream = StringIO() log_stream = StringIO()
for handler in logging.root.handlers[:]: for handler in logging.root.handlers[:]:
@ -32,5 +45,63 @@ def test_log():
log_text = log_stream.getvalue() log_text = log_stream.getvalue()
assert rand_string in log_text assert rand_string in log_text
if __name__ == "__main__":
test_log() def test_logging_defaults():
reset_logging()
app = Sanic("test_logging")
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()
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'
app = Sanic("test_logging", log_config=modified_config)
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']
@pytest.mark.parametrize('debug', (True, False, ))
def test_log_connection_lost(debug, monkeypatch):
""" Should not log Connection lost exception on non debug """
app = Sanic('connection_lost')
stream = StringIO()
root = logging.getLogger('root')
root.addHandler(logging.StreamHandler(stream))
monkeypatch.setattr(sanic.server, 'logger', root)
@app.route('/conn_lost')
async def conn_lost(request):
response = text('Ok')
response.output = Mock(side_effect=RuntimeError)
return response
with pytest.raises(ValueError):
# catch ValueError: Exception during request
app.test_client.get('/conn_lost', debug=debug)
log = stream.getvalue()
if debug:
assert log.startswith(
'Connection lost before response written @')
else:
assert 'Connection lost before response written @' not in log

View File

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

388
tests/test_named_routes.py Normal file
View File

@ -0,0 +1,388 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import asyncio
import pytest
from sanic import Sanic
from sanic.blueprints import Blueprint
from sanic.response import text
from sanic.exceptions import URLBuildError
from sanic.constants import HTTP_METHODS
# ------------------------------------------------------------ #
# UTF-8
# ------------------------------------------------------------ #
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_versioned_named_routes_get(method):
app = Sanic('test_shorhand_routes_get')
bp = Blueprint('test_bp', url_prefix='/bp')
method = method.lower()
route_name = 'route_{}'.format(method)
route_name2 = 'route2_{}'.format(method)
func = getattr(app, method)
if callable(func):
@func('/{}'.format(method), version=1, name=route_name)
def handler(request):
return text('OK')
else:
print(func)
raise
func = getattr(bp, method)
if callable(func):
@func('/{}'.format(method), version=1, name=route_name2)
def handler2(request):
return text('OK')
else:
print(func)
raise
app.blueprint(bp)
assert app.router.routes_all['/v1/{}'.format(method)].name == route_name
route = app.router.routes_all['/v1/bp/{}'.format(method)]
assert route.name == 'test_bp.{}'.format(route_name2)
assert app.url_for(route_name) == '/v1/{}'.format(method)
url = app.url_for('test_bp.{}'.format(route_name2))
assert url == '/v1/bp/{}'.format(method)
with pytest.raises(URLBuildError):
app.url_for('handler')
def test_shorthand_default_routes_get():
app = Sanic('test_shorhand_routes_get')
@app.get('/get')
def handler(request):
return text('OK')
assert app.router.routes_all['/get'].name == 'handler'
assert app.url_for('handler') == '/get'
def test_shorthand_named_routes_get():
app = Sanic('test_shorhand_routes_get')
bp = Blueprint('test_bp', url_prefix='/bp')
@app.get('/get', name='route_get')
def handler(request):
return text('OK')
@bp.get('/get', name='route_bp')
def handler2(request):
return text('Blueprint')
app.blueprint(bp)
assert app.router.routes_all['/get'].name == 'route_get'
assert app.url_for('route_get') == '/get'
with pytest.raises(URLBuildError):
app.url_for('handler')
assert app.router.routes_all['/bp/get'].name == 'test_bp.route_bp'
assert app.url_for('test_bp.route_bp') == '/bp/get'
with pytest.raises(URLBuildError):
app.url_for('test_bp.handler2')
def test_shorthand_named_routes_post():
app = Sanic('test_shorhand_routes_post')
@app.post('/post', name='route_name')
def handler(request):
return text('OK')
assert app.router.routes_all['/post'].name == 'route_name'
assert app.url_for('route_name') == '/post'
with pytest.raises(URLBuildError):
app.url_for('handler')
def test_shorthand_named_routes_put():
app = Sanic('test_shorhand_routes_put')
@app.put('/put', name='route_put')
def handler(request):
assert request.stream is None
return text('OK')
assert app.is_request_stream is False
assert app.router.routes_all['/put'].name == 'route_put'
assert app.url_for('route_put') == '/put'
with pytest.raises(URLBuildError):
app.url_for('handler')
def test_shorthand_named_routes_delete():
app = Sanic('test_shorhand_routes_delete')
@app.delete('/delete', name='route_delete')
def handler(request):
assert request.stream is None
return text('OK')
assert app.is_request_stream is False
assert app.router.routes_all['/delete'].name == 'route_delete'
assert app.url_for('route_delete') == '/delete'
with pytest.raises(URLBuildError):
app.url_for('handler')
def test_shorthand_named_routes_patch():
app = Sanic('test_shorhand_routes_patch')
@app.patch('/patch', name='route_patch')
def handler(request):
assert request.stream is None
return text('OK')
assert app.is_request_stream is False
assert app.router.routes_all['/patch'].name == 'route_patch'
assert app.url_for('route_patch') == '/patch'
with pytest.raises(URLBuildError):
app.url_for('handler')
def test_shorthand_named_routes_head():
app = Sanic('test_shorhand_routes_head')
@app.head('/head', name='route_head')
def handler(request):
assert request.stream is None
return text('OK')
assert app.is_request_stream is False
assert app.router.routes_all['/head'].name == 'route_head'
assert app.url_for('route_head') == '/head'
with pytest.raises(URLBuildError):
app.url_for('handler')
def test_shorthand_named_routes_options():
app = Sanic('test_shorhand_routes_options')
@app.options('/options', name='route_options')
def handler(request):
assert request.stream is None
return text('OK')
assert app.is_request_stream is False
assert app.router.routes_all['/options'].name == 'route_options'
assert app.url_for('route_options') == '/options'
with pytest.raises(URLBuildError):
app.url_for('handler')
def test_named_static_routes():
app = Sanic('test_dynamic_route')
@app.route('/test', name='route_test')
async def handler1(request):
return text('OK1')
@app.route('/pizazz', name='route_pizazz')
async def handler2(request):
return text('OK2')
assert app.router.routes_all['/test'].name == 'route_test'
assert app.router.routes_static['/test'].name == 'route_test'
assert app.url_for('route_test') == '/test'
with pytest.raises(URLBuildError):
app.url_for('handler1')
assert app.router.routes_all['/pizazz'].name == 'route_pizazz'
assert app.router.routes_static['/pizazz'].name == 'route_pizazz'
assert app.url_for('route_pizazz') == '/pizazz'
with pytest.raises(URLBuildError):
app.url_for('handler2')
def test_named_dynamic_route():
app = Sanic('test_dynamic_route')
results = []
@app.route('/folder/<name>', name='route_dynamic')
async def handler(request, name):
results.append(name)
return text('OK')
assert app.router.routes_all['/folder/<name>'].name == 'route_dynamic'
assert app.url_for('route_dynamic', name='test') == '/folder/test'
with pytest.raises(URLBuildError):
app.url_for('handler')
def test_dynamic_named_route_regex():
app = Sanic('test_dynamic_route_regex')
@app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>', name='route_re')
async def handler(request, folder_id):
return text('OK')
route = app.router.routes_all['/folder/<folder_id:[A-Za-z0-9]{0,4}>']
assert route.name == 'route_re'
assert app.url_for('route_re', folder_id='test') == '/folder/test'
with pytest.raises(URLBuildError):
app.url_for('handler')
def test_dynamic_named_route_path():
app = Sanic('test_dynamic_route_path')
@app.route('/<path:path>/info', name='route_dynamic_path')
async def handler(request, path):
return text('OK')
route = app.router.routes_all['/<path:path>/info']
assert route.name == 'route_dynamic_path'
assert app.url_for('route_dynamic_path', path='path/1') == '/path/1/info'
with pytest.raises(URLBuildError):
app.url_for('handler')
def test_dynamic_named_route_unhashable():
app = Sanic('test_dynamic_route_unhashable')
@app.route('/folder/<unhashable:[A-Za-z0-9/]+>/end/',
name='route_unhashable')
async def handler(request, unhashable):
return text('OK')
route = app.router.routes_all['/folder/<unhashable:[A-Za-z0-9/]+>/end/']
assert route.name == 'route_unhashable'
url = app.url_for('route_unhashable', unhashable='test/asdf')
assert url == '/folder/test/asdf/end'
with pytest.raises(URLBuildError):
app.url_for('handler')
def test_websocket_named_route():
app = Sanic('test_websocket_route')
ev = asyncio.Event()
@app.websocket('/ws', name='route_ws')
async def handler(request, ws):
assert ws.subprotocol is None
ev.set()
assert app.router.routes_all['/ws'].name == 'route_ws'
assert app.url_for('route_ws') == '/ws'
with pytest.raises(URLBuildError):
app.url_for('handler')
def test_websocket_named_route_with_subprotocols():
app = Sanic('test_websocket_route')
results = []
@app.websocket('/ws', subprotocols=['foo', 'bar'], name='route_ws')
async def handler(request, ws):
results.append(ws.subprotocol)
assert app.router.routes_all['/ws'].name == 'route_ws'
assert app.url_for('route_ws') == '/ws'
with pytest.raises(URLBuildError):
app.url_for('handler')
def test_static_add_named_route():
app = Sanic('test_static_add_route')
async def handler1(request):
return text('OK1')
async def handler2(request):
return text('OK2')
app.add_route(handler1, '/test', name='route_test')
app.add_route(handler2, '/test2', name='route_test2')
assert app.router.routes_all['/test'].name == 'route_test'
assert app.router.routes_static['/test'].name == 'route_test'
assert app.url_for('route_test') == '/test'
with pytest.raises(URLBuildError):
app.url_for('handler1')
assert app.router.routes_all['/test2'].name == 'route_test2'
assert app.router.routes_static['/test2'].name == 'route_test2'
assert app.url_for('route_test2') == '/test2'
with pytest.raises(URLBuildError):
app.url_for('handler2')
def test_dynamic_add_named_route():
app = Sanic('test_dynamic_add_route')
results = []
async def handler(request, name):
results.append(name)
return text('OK')
app.add_route(handler, '/folder/<name>', name='route_dynamic')
assert app.router.routes_all['/folder/<name>'].name == 'route_dynamic'
assert app.url_for('route_dynamic', name='test') == '/folder/test'
with pytest.raises(URLBuildError):
app.url_for('handler')
def test_dynamic_add_named_route_unhashable():
app = Sanic('test_dynamic_add_route_unhashable')
async def handler(request, unhashable):
return text('OK')
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/',
name='route_unhashable')
route = app.router.routes_all['/folder/<unhashable:[A-Za-z0-9/]+>/end/']
assert route.name == 'route_unhashable'
url = app.url_for('route_unhashable', unhashable='folder1')
assert url == '/folder/folder1/end'
with pytest.raises(URLBuildError):
app.url_for('handler')
def test_overload_routes():
app = Sanic('test_dynamic_route')
@app.route('/overload', methods=['GET'], name='route_first')
async def handler1(request):
return text('OK1')
@app.route('/overload', methods=['POST', 'PUT'], name='route_second')
async def handler1(request):
return text('OK2')
request, response = app.test_client.get(app.url_for('route_first'))
assert response.text == 'OK1'
request, response = app.test_client.post(app.url_for('route_first'))
assert response.text == 'OK2'
request, response = app.test_client.put(app.url_for('route_first'))
assert response.text == 'OK2'
request, response = app.test_client.get(app.url_for('route_second'))
assert response.text == 'OK1'
request, response = app.test_client.post(app.url_for('route_second'))
assert response.text == 'OK2'
request, response = app.test_client.put(app.url_for('route_second'))
assert response.text == 'OK2'
assert app.router.routes_all['/overload'].name == 'route_first'
with pytest.raises(URLBuildError):
app.url_for('handler1')
assert app.url_for('route_first') == '/overload'
assert app.url_for('route_second') == app.url_for('route_first')

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

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

View File

@ -35,6 +35,25 @@ async def sample_streaming_fn(response):
await asyncio.sleep(.001) await asyncio.sleep(.001)
response.write('bar') response.write('bar')
def test_method_not_allowed():
app = Sanic('method_not_allowed')
@app.get('/')
async def test(request):
return response.json({'hello': 'world'})
request, response = app.test_client.head('/')
assert response.headers['Allow']== 'GET'
@app.post('/')
async def test(request):
return response.json({'hello': 'world'})
request, response = app.test_client.head('/')
assert response.status == 405
assert set(response.headers['Allow'].split(', ')) == set(['GET', 'POST'])
assert response.headers['Content-Length'] == '0'
@pytest.fixture @pytest.fixture
def json_app(): def json_app():
@ -149,7 +168,22 @@ def test_file_response(file_name, static_file_directory):
request, response = app.test_client.get('/files/{}'.format(file_name)) request, response = app.test_client.get('/files/{}'.format(file_name))
assert response.status == 200 assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name) assert response.body == get_file_content(static_file_directory, file_name)
assert 'Content-Disposition' not in response.headers
@pytest.mark.parametrize('source,dest', [
('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')])
def test_file_response_custom_filename(source, dest, static_file_directory):
app = Sanic('test_file_helper')
@app.route('/files/<filename>', methods=['GET'])
def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
file_path = os.path.abspath(unquote(file_path))
return file(file_path, filename=dest)
request, response = app.test_client.get('/files/{}'.format(source))
assert response.status == 200
assert response.body == get_file_content(static_file_directory, source)
assert response.headers['Content-Disposition'] == 'attachment; filename="{}"'.format(dest)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_file_head_response(file_name, static_file_directory): def test_file_head_response(file_name, static_file_directory):
@ -191,7 +225,22 @@ def test_file_stream_response(file_name, static_file_directory):
request, response = app.test_client.get('/files/{}'.format(file_name)) request, response = app.test_client.get('/files/{}'.format(file_name))
assert response.status == 200 assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name) assert response.body == get_file_content(static_file_directory, file_name)
assert 'Content-Disposition' not in response.headers
@pytest.mark.parametrize('source,dest', [
('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')])
def test_file_stream_response_custom_filename(source, dest, static_file_directory):
app = Sanic('test_file_helper')
@app.route('/files/<filename>', methods=['GET'])
def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
file_path = os.path.abspath(unquote(file_path))
return file_stream(file_path, chunk_size=32, filename=dest)
request, response = app.test_client.get('/files/{}'.format(source))
assert response.status == 200
assert response.body == get_file_content(static_file_directory, source)
assert response.headers['Content-Disposition'] == 'attachment; filename="{}"'.format(dest)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_file_stream_head_response(file_name, static_file_directory): def test_file_stream_head_response(file_name, static_file_directory):
@ -224,4 +273,4 @@ def test_file_stream_head_response(file_name, static_file_directory):
assert 'Content-Length' in response.headers assert 'Content-Length' in response.headers
assert int(response.headers[ assert int(response.headers[
'Content-Length']) == len( 'Content-Length']) == len(
get_file_content(static_file_directory, file_name)) get_file_content(static_file_directory, file_name))

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

@ -44,6 +44,24 @@ def test_shorthand_routes_get():
request, response = app.test_client.post('/get') request, response = app.test_client.post('/get')
assert response.status == 405 assert response.status == 405
def test_shorthand_routes_multiple():
app = Sanic('test_shorthand_routes_multiple')
@app.get('/get')
def get_handler(request):
return text('OK')
@app.options('/get')
def options_handler(request):
return text('')
request, response = app.test_client.get('/get/')
assert response.status == 200
assert response.text == 'OK'
request, response = app.test_client.options('/get/')
assert response.status == 200
def test_route_strict_slash(): def test_route_strict_slash():
app = Sanic('test_route_strict_slash') app = Sanic('test_route_strict_slash')
@ -71,6 +89,46 @@ def test_route_strict_slash():
request, response = app.test_client.post('/post') request, response = app.test_client.post('/post')
assert response.status == 404 assert response.status == 404
def test_route_invalid_parameter_syntax():
with pytest.raises(ValueError):
app = Sanic('test_route_invalid_param_syntax')
@app.get('/get/<:string>', strict_slashes=True)
def handler(request):
return text('OK')
request, response = app.test_client.get('/get')
def test_route_strict_slash_default_value():
app = Sanic('test_route_strict_slash', strict_slashes=True)
@app.get('/get')
def handler(request):
return text('OK')
request, response = app.test_client.get('/get/')
assert response.status == 404
def test_route_strict_slash_without_passing_default_value():
app = Sanic('test_route_strict_slash')
@app.get('/get')
def handler(request):
return text('OK')
request, response = app.test_client.get('/get/')
assert response.text == 'OK'
def test_route_strict_slash_default_value_can_be_overwritten():
app = Sanic('test_route_strict_slash', strict_slashes=True)
@app.get('/get', strict_slashes=False)
def handler(request):
return text('OK')
request, response = app.test_client.get('/get/')
assert response.text == 'OK'
def test_route_optional_slash(): def test_route_optional_slash():
app = Sanic('test_route_optional_slash') app = Sanic('test_route_optional_slash')
@ -341,6 +399,7 @@ def test_websocket_route():
@app.websocket('/ws') @app.websocket('/ws')
async def handler(request, ws): async def handler(request, ws):
assert ws.subprotocol is None
ev.set() ev.set()
request, response = app.test_client.get('/ws', headers={ request, response = app.test_client.get('/ws', headers={
@ -352,6 +411,48 @@ def test_websocket_route():
assert ev.is_set() assert ev.is_set()
def test_websocket_route_with_subprotocols():
app = Sanic('test_websocket_route')
results = []
@app.websocket('/ws', subprotocols=['foo', 'bar'])
async def handler(request, ws):
results.append(ws.subprotocol)
request, response = app.test_client.get('/ws', headers={
'Upgrade': 'websocket',
'Connection': 'upgrade',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13',
'Sec-WebSocket-Protocol': 'bar'})
assert response.status == 101
request, response = app.test_client.get('/ws', headers={
'Upgrade': 'websocket',
'Connection': 'upgrade',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13',
'Sec-WebSocket-Protocol': 'bar, foo'})
assert response.status == 101
request, response = app.test_client.get('/ws', headers={
'Upgrade': 'websocket',
'Connection': 'upgrade',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13',
'Sec-WebSocket-Protocol': 'baz'})
assert response.status == 101
request, response = app.test_client.get('/ws', headers={
'Upgrade': 'websocket',
'Connection': 'upgrade',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13'})
assert response.status == 101
assert results == ['bar', 'bar', None, None]
def test_route_duplicate(): def test_route_duplicate():
app = Sanic('test_route_duplicate') app = Sanic('test_route_duplicate')
@ -671,6 +772,7 @@ def test_remove_route_without_clean_cache():
assert response.status == 200 assert response.status == 200
app.remove_route('/test', clean_cache=True) app.remove_route('/test', clean_cache=True)
app.remove_route('/test/', clean_cache=True)
request, response = app.test_client.get('/test') request, response = app.test_client.get('/test')
assert response.status == 404 assert response.status == 404

View File

@ -59,3 +59,20 @@ def test_all_listeners():
start_stop_app(random_name_app) start_stop_app(random_name_app)
for listener_name in AVAILABLE_LISTENERS: for listener_name in AVAILABLE_LISTENERS:
assert random_name_app.name + listener_name == output.pop() assert random_name_app.name + listener_name == output.pop()
async def test_trigger_before_events_create_server():
class MySanicDb:
pass
app = Sanic("test_sanic_app")
@app.listener('before_server_start')
async def init_db(app, loop):
app.db = MySanicDb()
await app.create_server()
assert hasattr(app, "db")
assert isinstance(app.db, MySanicDb)

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_specified_host(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

@ -5,7 +5,7 @@ from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.views import HTTPMethodView from sanic.views import HTTPMethodView
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.testing import PORT as test_port from sanic.testing import PORT as test_port, HOST as test_host
from sanic.exceptions import URLBuildError from sanic.exceptions import URLBuildError
import string import string
@ -15,8 +15,11 @@ URL_FOR_VALUE1 = '/myurl?arg1=v1&arg1=v2'
URL_FOR_ARGS2 = dict(arg1=['v1', 'v2'], _anchor='anchor') URL_FOR_ARGS2 = dict(arg1=['v1', 'v2'], _anchor='anchor')
URL_FOR_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor' URL_FOR_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor'
URL_FOR_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http', URL_FOR_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http',
_server='localhost:{}'.format(test_port), _external=True) _server='{}:{}'.format(test_host, test_port), _external=True)
URL_FOR_VALUE3 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(test_port) URL_FOR_VALUE3 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port)
URL_FOR_ARGS4 = dict(arg1='v1', _anchor='anchor', _external=True,
_server='http://{}:{}'.format(test_host, test_port),)
URL_FOR_VALUE4 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port)
def _generate_handlers_from_names(app, l): 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)),)

View File

@ -131,5 +131,5 @@ def test_worker_close(worker):
loop.run_until_complete(_close) loop.run_until_complete(_close)
assert worker.signal.stopped == True assert worker.signal.stopped == True
conn.websocket.close_connection.assert_called_with(force=True) conn.websocket.close_connection.assert_called_with(after_handshake=False)
assert len(worker.servers) == 0 assert len(worker.servers) == 0

View File

@ -10,13 +10,16 @@ deps =
coverage coverage
pytest pytest
pytest-cov pytest-cov
pytest-sanic
pytest-sugar pytest-sugar
aiohttp==1.3.5 aiohttp==1.3.5
chardet<=2.3.0 chardet<=2.3.0
beautifulsoup4 beautifulsoup4
gunicorn gunicorn
commands = commands =
pytest tests --cov sanic --cov-report term-missing {posargs} pytest tests --cov sanic --cov-report= {posargs}
- coverage combine --append
coverage report -m
[testenv:flake8] [testenv:flake8]
deps = deps =