Merge branch 'master' into sphinx-docs

This commit is contained in:
Cadel Watson 2017-01-19 08:48:54 +11:00
commit 9d4b104d2d
38 changed files with 1278 additions and 174 deletions

4
.gitignore vendored
View File

@ -1,13 +1,15 @@
*~ *~
*.egg-info *.egg-info
*.egg *.egg
*.eggs
*.pyc
.coverage .coverage
.coverage.* .coverage.*
coverage coverage
.tox .tox
settings.py settings.py
*.pyc
.idea/* .idea/*
.cache/* .cache/*
.python-version
docs/_build/ docs/_build/
docs/_api/ docs/_api/

View File

@ -1,15 +1,10 @@
sudo: false
language: python language: python
python: python:
- '3.5' - '3.5'
- '3.6' - '3.6'
install: install: pip install tox-travis
- pip install -r requirements.txt script: tox
- pip install -r requirements-dev.txt
- python setup.py install
- pip install flake8
- pip install pytest
before_script: flake8 sanic
script: py.test -v tests
deploy: deploy:
provider: pypi provider: pypi
user: channelcat user: channelcat

View File

@ -74,3 +74,37 @@ Documentation can be found in the ``docs`` directory.
: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/
TODO
----
* Streamed file processing
* File output
* Examples of integrations with 3rd-party modules
* RESTful router
Limitations
-----------
* No wheels for uvloop and httptools on Windows :(
Final Thoughts
--------------
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
▌ ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
▀▀█████████▀
▄▄██▀██████▀█
▄██▀ ▀▀▀ █
▄█ ▐▌
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
▌ ▐ ▀▀▄▄▄▀
▀▀▄▄▀

View File

@ -28,7 +28,7 @@ class SimpleView(HTTPMethodView):
def delete(self, request): def delete(self, request):
return text('I am delete method') return text('I am delete method')
app.add_route(SimpleView(), '/') app.add_route(SimpleView.as_view(), '/')
``` ```
@ -40,6 +40,19 @@ class NameView(HTTPMethodView):
def get(self, request, name): def get(self, request, name):
return text('Hello {}'.format(name)) return text('Hello {}'.format(name))
app.add_route(NameView(), '/<name>') app.add_route(NameView.as_view(), '/<name>')
```
If you want to add decorator for class, you could set decorators variable
```
class ViewWithDecorator(HTTPMethodView):
decorators = [some_decorator_here]
def get(self, request, name):
return text('Hello I have a decorator')
app.add_route(ViewWithDecorator.as_view(), '/url')
``` ```

70
docs/custom_protocol.md Normal file
View File

@ -0,0 +1,70 @@
# Custom Protocol
You can change the behavior of protocol by using custom protocol.
If you want to use custom protocol, you should put subclass of [protocol class](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes) in the protocol keyword argument of `sanic.run()`. The constructor of custom protocol class gets following keyword arguments from Sanic.
* loop
`loop` is an asyncio compatible event loop.
* connections
`connections` is a `set object` to store protocol objects.
When Sanic receives `SIGINT` or `SIGTERM`, Sanic executes `protocol.close_if_idle()` for a `protocol objects` stored in connections.
* signal
`signal` is a `sanic.server.Signal object` with `stopped attribute`.
When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`.
* request_handler
`request_handler` is a coroutine that takes a `sanic.request.Request` object and a `response callback` as arguments.
* error_handler
`error_handler` is a `sanic.exceptions.Handler` object.
* request_timeout
`request_timeout` is seconds for timeout.
* request_max_size
`request_max_size` is bytes of max request size.
## Example
By default protocol, an error occurs, if the handler does not return an `HTTPResponse object`.
In this example, By rewriting `write_response()`, if the handler returns `str`, it will be converted to an `HTTPResponse object`.
```python
from sanic import Sanic
from sanic.server import HttpProtocol
from sanic.response import text
app = Sanic(__name__)
class CustomHttpProtocol(HttpProtocol):
def __init__(self, *, loop, request_handler, error_handler,
signal, connections, request_timeout, request_max_size):
super().__init__(
loop=loop, request_handler=request_handler,
error_handler=error_handler, signal=signal,
connections=connections, request_timeout=request_timeout,
request_max_size=request_max_size)
def write_response(self, response):
if isinstance(response, str):
response = text(response)
self.transport.write(
response.output(self.request.version)
)
self.transport.close()
@app.route('/')
async def string(request):
return 'string'
@app.route('/1')
async def response(request):
return text('response')
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)
```

6
docs/extensions.md Normal file
View File

@ -0,0 +1,6 @@
# Sanic Extensions
A list of Sanic extensions created by the community.
* [Sessions](https://github.com/subyraman/sanic_session) &mdash; Support for sessions. Allows using redis, memcache or an in memory store.
* [CORS](https://github.com/ashleysommer/sanic-cors) &mdash; A port of flask-cors.

View File

@ -9,6 +9,7 @@ The following request variables are accessible as properties:
`request.args` (dict) - Query String variables. Use getlist to get multiple of the same name `request.args` (dict) - Query String variables. Use getlist to get multiple of the same name
`request.form` (dict) - Posted form variables. Use getlist to get multiple of the same name `request.form` (dict) - Posted form variables. Use getlist to get multiple of the same name
`request.body` (bytes) - Posted raw body. To get the raw data, regardless of content type `request.body` (bytes) - Posted raw body. To get the raw data, regardless of content type
`request.ip` (str) - IP address of the requester
See request.py for more information See request.py for more information

View File

@ -33,12 +33,12 @@ async def handler1(request):
return text('OK') return text('OK')
app.add_route(handler1, '/test') app.add_route(handler1, '/test')
async def handler(request, name): async def handler2(request, name):
return text('Folder - {}'.format(name)) return text('Folder - {}'.format(name))
app.add_route(handler, '/folder/<name>') app.add_route(handler2, '/folder/<name>')
async def person_handler(request, name): async def person_handler2(request, name):
return text('Person - {}'.format(name)) return text('Person - {}'.format(name))
app.add_route(handler, '/person/<name:[A-z]>') app.add_route(person_handler2, '/person/<name:[A-z]>')
``` ```

View File

@ -0,0 +1,23 @@
from sanic import Sanic
from sanic.response import text
import json
import logging
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
logging_format += "%(module)s::%(funcName)s():l%(lineno)d: "
logging_format += "%(message)s"
logging.basicConfig(
format=logging_format,
level=logging.DEBUG
)
log = logging.getLogger()
# Set logger to override default basicConfig
sanic = Sanic()
@sanic.route("/")
def test(request):
log.info("received request; responding with 'hey'")
return text("hey")
sanic.run(host="0.0.0.0", port=8000)

View File

@ -0,0 +1,65 @@
""" To run this example you need additional asyncpg package
"""
import os
import asyncio
import uvloop
from asyncpg import create_pool
from sanic import Sanic
from sanic.response import json
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
DB_CONFIG = {
'host': '<host>',
'user': '<username>',
'password': '<password>',
'port': '<port>',
'database': '<database>'
}
def jsonify(records):
""" Parse asyncpg record response into JSON format
"""
return [{key: value for key, value in
zip(r.keys(), r.values())} for r in records]
loop = asyncio.get_event_loop()
async def make_pool():
return await create_pool(**DB_CONFIG)
app = Sanic(__name__)
pool = loop.run_until_complete(make_pool())
async def create_db():
""" Create some table and add some data
"""
async with pool.acquire() as connection:
async with connection.transaction():
await connection.execute('DROP TABLE IF EXISTS sanic_post')
await connection.execute("""CREATE TABLE sanic_post (
id serial primary key,
content varchar(50),
post_date timestamp
);""")
for i in range(0, 100):
await connection.execute(f"""INSERT INTO sanic_post
(id, content, post_date) VALUES ({i}, {i}, now())""")
@app.route("/")
async def handler(request):
async with pool.acquire() as connection:
async with connection.transaction():
results = await connection.fetch('SELECT * FROM sanic_post')
return json({'posts': jsonify(results)})
if __name__ == '__main__':
loop.run_until_complete(create_db())
app.run(host='0.0.0.0', port=8000, loop=loop)

View File

@ -64,11 +64,11 @@ def query_string(request):
# Run Server # Run Server
# ----------------------------------------------- # # ----------------------------------------------- #
def after_start(loop): def after_start(app, loop):
log.info("OH OH OH OH OHHHHHHHH") log.info("OH OH OH OH OHHHHHHHH")
def before_stop(loop): def before_stop(app, loop):
log.info("TRIED EVERYTHING") log.info("TRIED EVERYTHING")

32
examples/vhosts.py Normal file
View File

@ -0,0 +1,32 @@
from sanic.response import text
from sanic import Sanic
from sanic.blueprints import Blueprint
# Usage
# curl -H "Host: example.com" localhost:8000
# curl -H "Host: sub.example.com" localhost:8000
# curl -H "Host: bp.example.com" localhost:8000/question
# curl -H "Host: bp.example.com" localhost:8000/answer
app = Sanic()
bp = Blueprint("bp", host="bp.example.com")
@app.route('/', host="example.com")
async def hello(request):
return text("Answer")
@app.route('/', host="sub.example.com")
async def hello(request):
return text("42")
@bp.route("/question")
async def hello(request):
return text("What is the meaning of life?")
@bp.route("/answer")
async def hello(request):
return text("42")
app.register_blueprint(bp)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@ -14,3 +14,4 @@ tornado
aiofiles aiofiles
sphinx sphinx
recommonmark recommonmark
beautifulsoup4

View File

@ -2,4 +2,3 @@ httptools
ujson ujson
uvloop uvloop
aiofiles aiofiles
multidict

View File

@ -1,6 +1,6 @@
from .sanic import Sanic from .sanic import Sanic
from .blueprints import Blueprint from .blueprints import Blueprint
__version__ = '0.1.9' __version__ = '0.2.0'
__all__ = ['Sanic', 'Blueprint'] __all__ = ['Sanic', 'Blueprint']

View File

@ -20,7 +20,7 @@ if __name__ == "__main__":
module = import_module(module_name) module = import_module(module_name)
app = getattr(module, app_name, None) app = getattr(module, app_name, None)
if type(app) is not Sanic: if not isinstance(app, Sanic):
raise ValueError("Module is not a Sanic app, it is a {}. " raise ValueError("Module is not a Sanic app, it is a {}. "
"Perhaps you meant {}.app?" "Perhaps you meant {}.app?"
.format(type(app).__name__, args.module)) .format(type(app).__name__, args.module))

View File

@ -18,14 +18,17 @@ class BlueprintSetup:
#: blueprint. #: blueprint.
self.url_prefix = url_prefix self.url_prefix = url_prefix
def add_route(self, handler, uri, methods): def add_route(self, handler, uri, methods, host=None):
""" """
A helper method to register a handler to the application url routes. A helper method to register a handler to the application url routes.
""" """
if self.url_prefix: if self.url_prefix:
uri = self.url_prefix + uri uri = self.url_prefix + uri
self.app.route(uri=uri, methods=methods)(handler) if host is None:
host = self.blueprint.host
self.app.route(uri=uri, methods=methods, host=host)(handler)
def add_exception(self, handler, *args, **kwargs): def add_exception(self, handler, *args, **kwargs):
""" """
@ -53,7 +56,7 @@ class BlueprintSetup:
class Blueprint: class Blueprint:
def __init__(self, name, url_prefix=None): def __init__(self, name, url_prefix=None, host=None):
""" """
Creates a new blueprint Creates a new blueprint
:param name: Unique name of the blueprint :param name: Unique name of the blueprint
@ -63,6 +66,7 @@ class Blueprint:
self.url_prefix = url_prefix self.url_prefix = url_prefix
self.deferred_functions = [] self.deferred_functions = []
self.listeners = defaultdict(list) self.listeners = defaultdict(list)
self.host = host
def record(self, func): def record(self, func):
""" """
@ -83,18 +87,18 @@ class Blueprint:
for deferred in self.deferred_functions: for deferred in self.deferred_functions:
deferred(state) deferred(state)
def route(self, uri, methods=None): def route(self, uri, methods=None, host=None):
""" """
""" """
def decorator(handler): def decorator(handler):
self.record(lambda s: s.add_route(handler, uri, methods)) self.record(lambda s: s.add_route(handler, uri, methods, host))
return handler return handler
return decorator return decorator
def add_route(self, handler, uri, methods=None): def add_route(self, handler, uri, methods=None, host=None):
""" """
""" """
self.record(lambda s: s.add_route(handler, uri, methods)) self.record(lambda s: s.add_route(handler, uri, methods, host))
return handler return handler
def listener(self, event): def listener(self, event):

View File

@ -1,5 +1,104 @@
from .response import text from .response import text, html
from traceback import format_exc from .log import log
from traceback import format_exc, extract_tb
import sys
TRACEBACK_STYLE = '''
<style>
body {
padding: 20px;
font-family: Arial, sans-serif;
}
p {
margin: 0;
}
.summary {
padding: 10px;
}
h1 {
margin-bottom: 0;
}
h3 {
margin-top: 10px;
}
h3 code {
font-size: 24px;
}
.frame-line > * {
padding: 5px 10px;
}
.frame-line {
margin-bottom: 5px;
}
.frame-code {
font-size: 16px;
padding-left: 30px;
}
.tb-wrapper {
border: 1px solid #f3f3f3;
}
.tb-header {
background-color: #f3f3f3;
padding: 5px 10px;
}
.frame-descriptor {
background-color: #e2eafb;
}
.frame-descriptor {
font-size: 14px;
}
</style>
'''
TRACEBACK_WRAPPER_HTML = '''
<html>
<head>
{style}
</head>
<body>
<h1>{exc_name}</h1>
<h3><code>{exc_value}</code></h3>
<div class="tb-wrapper">
<p class="tb-header">Traceback (most recent call last):</p>
{frame_html}
<p class="summary">
<b>{exc_name}: {exc_value}</b>
while handling uri <code>{uri}</code>
</p>
</div>
</body>
</html>
'''
TRACEBACK_LINE_HTML = '''
<div class="frame-line">
<p class="frame-descriptor">
File {0.filename}, line <i>{0.lineno}</i>,
in <code><b>{0.name}</b></code>
</p>
<p class="frame-code"><code>{0.line}</code></p>
</div>
'''
INTERNAL_SERVER_ERROR_HTML = '''
<h1>Internal Server Error</h1>
<p>
The server encountered an internal error and cannot complete
your request.
</p>
'''
class SanicException(Exception): class SanicException(Exception):
@ -45,6 +144,21 @@ class Handler:
self.handlers = {} self.handlers = {}
self.sanic = sanic self.sanic = sanic
def _render_traceback_html(self, exception, request):
exc_type, exc_value, tb = sys.exc_info()
frames = extract_tb(tb)
frame_html = []
for frame in frames:
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
return TRACEBACK_WRAPPER_HTML.format(
style=TRACEBACK_STYLE,
exc_name=exc_type.__name__,
exc_value=exc_value,
frame_html=''.join(frame_html),
uri=request.url)
def add(self, exception, handler): def add(self, exception, handler):
self.handlers[exception] = handler self.handlers[exception] = handler
@ -57,18 +171,32 @@ class Handler:
:return: Response object :return: Response object
""" """
handler = self.handlers.get(type(exception), self.default) handler = self.handlers.get(type(exception), self.default)
try:
response = handler(request=request, exception=exception) response = handler(request=request, exception=exception)
except:
if self.sanic.debug:
response_message = (
'Exception raised in exception handler "{}" '
'for uri: "{}"\n{}').format(
handler.__name__, request.url, format_exc())
log.error(response_message)
return text(response_message, 500)
else:
return text('An error occurred while handling an error', 500)
return response return response
def default(self, request, exception): def default(self, request, exception):
if issubclass(type(exception), SanicException): if issubclass(type(exception), SanicException):
return text( return text(
"Error: {}".format(exception), 'Error: {}'.format(exception),
status=getattr(exception, 'status_code', 500)) status=getattr(exception, 'status_code', 500))
elif self.sanic.debug: elif self.sanic.debug:
return text( html_output = self._render_traceback_html(exception, request)
"Error: {}\nException: {}".format(
exception, format_exc()), status=500) response_message = (
'Exception occurred while handling uri: "{}"\n{}'.format(
request.url, format_exc()))
log.error(response_message)
return html(html_output, status=500)
else: else:
return text( return html(INTERNAL_SERVER_ERROR_HTML, status=500)
"An error occurred while generating the request", status=500)

View File

@ -1,5 +1,3 @@
import logging import logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s")
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@ -25,6 +25,9 @@ class RequestParameters(dict):
self.super = super() self.super = super()
self.super.__init__(*args, **kwargs) self.super.__init__(*args, **kwargs)
def __getitem__(self, name):
return self.get(name)
def get(self, name, default=None): def get(self, name, default=None):
values = self.super.get(name) values = self.super.get(name)
return values[0] if values else default return values[0] if values else default
@ -38,18 +41,20 @@ class Request(dict):
Properties of an HTTP request such as URL, headers, etc. Properties of an HTTP request such as URL, headers, etc.
""" """
__slots__ = ( __slots__ = (
'url', 'headers', 'version', 'method', '_cookies', 'url', 'headers', 'version', 'method', '_cookies', 'transport',
'query_string', 'body', 'query_string', 'body',
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
'_ip',
) )
def __init__(self, url_bytes, headers, version, method): def __init__(self, url_bytes, headers, version, method, transport):
# TODO: Content-Encoding detection # TODO: Content-Encoding detection
url_parsed = parse_url(url_bytes) url_parsed = parse_url(url_bytes)
self.url = url_parsed.path.decode('utf-8') self.url = url_parsed.path.decode('utf-8')
self.headers = headers self.headers = headers
self.version = version self.version = version
self.method = method self.method = method
self.transport = transport
self.query_string = None self.query_string = None
if url_parsed.query: if url_parsed.query:
self.query_string = url_parsed.query.decode('utf-8') self.query_string = url_parsed.query.decode('utf-8')
@ -64,7 +69,7 @@ class Request(dict):
@property @property
def json(self): def json(self):
if not self.parsed_json: if self.parsed_json is None:
try: try:
self.parsed_json = json_loads(self.body) self.parsed_json = json_loads(self.body)
except Exception: except Exception:
@ -72,6 +77,17 @@ class Request(dict):
return self.parsed_json return self.parsed_json
@property
def token(self):
"""
Attempts to return the auth header token.
:return: token related to request
"""
auth_header = self.headers.get('Authorization')
if auth_header is not None:
return auth_header.split()[1]
return auth_header
@property @property
def form(self): def form(self):
if self.parsed_form is None: if self.parsed_form is None:
@ -125,6 +141,12 @@ class Request(dict):
self._cookies = {} self._cookies = {}
return self._cookies return self._cookies
@property
def ip(self):
if not hasattr(self, '_ip'):
self._ip = self.transport.get_extra_info('peername')
return self._ip
File = namedtuple('File', ['type', 'body', 'name']) File = namedtuple('File', ['type', 'body', 'name'])

View File

@ -83,10 +83,10 @@ class HTTPResponse:
if body is not None: if body is not None:
try: try:
# Try to encode it regularly # Try to encode it regularly
self.body = body.encode('utf-8') self.body = body.encode()
except AttributeError: except AttributeError:
# Convert it to a str if you can't # Convert it to a str if you can't
self.body = str(body).encode('utf-8') self.body = str(body).encode()
else: else:
self.body = body_bytes self.body = body_bytes
@ -103,10 +103,14 @@ class HTTPResponse:
headers = b'' headers = b''
if self.headers: if self.headers:
headers = b''.join( for name, value in self.headers.items():
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) try:
for name, value in self.headers.items() headers += (
) b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')))
except AttributeError:
headers += (
b'%b: %b\r\n' % (
str(name).encode(), str(value).encode('utf-8')))
# Try to pull from the common codes first # Try to pull from the common codes first
# Speeds up response rate 6% over pulling from all # Speeds up response rate 6% over pulling from all
@ -165,3 +169,26 @@ async def file(location, mime_type=None, headers=None):
headers=headers, headers=headers,
content_type=mime_type, content_type=mime_type,
body_bytes=out_stream) body_bytes=out_stream)
def redirect(to, headers=None, status=302,
content_type="text/html; charset=utf-8"):
"""
Aborts execution and causes a 302 redirect (by default).
:param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request
:param status: status code (int) of the new request, defaults to 302
:param content_type:
the content type (string) of the response
:returns: the redirecting Response
"""
headers = headers or {}
# According to RFC 7231, a relative URI is now permitted.
headers['Location'] = to
return HTTPResponse(
status=status,
headers=headers,
content_type=content_type)

View File

@ -23,6 +23,10 @@ class RouteExists(Exception):
pass pass
class RouteDoesNotExist(Exception):
pass
class Router: class Router:
""" """
Router supports basic routing with parameters and method checks Router supports basic routing with parameters and method checks
@ -31,16 +35,16 @@ class Router:
.. code-block:: python .. code-block:: python
@sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...]) @sanic.route('/my/url/<my_param>', methods=['GET', 'POST', ...])
def my_route(request, my_parameter): def my_route(request, my_param):
do stuff... do stuff...
or or
.. code-block:: python .. code-block:: python
@sanic.route('/my/url/<my_paramter>:type', methods['GET', 'POST', ...]) @sanic.route('/my/url/<my_param:my_type>', methods['GET', 'POST', ...])
def my_route_with_type(request, my_parameter): def my_route_with_type(request, my_param: my_type):
do stuff... do stuff...
Parameters will be passed as keyword arguments to the request handling Parameters will be passed as keyword arguments to the request handling
@ -59,8 +63,9 @@ class Router:
self.routes_static = {} self.routes_static = {}
self.routes_dynamic = defaultdict(list) self.routes_dynamic = defaultdict(list)
self.routes_always_check = [] self.routes_always_check = []
self.hosts = None
def add(self, uri, methods, handler): def add(self, uri, methods, handler, host=None):
""" """
Adds a handler to the route list Adds a handler to the route list
@ -71,6 +76,17 @@ class Router:
When executed, it should provide a response object. When executed, it should provide a response object.
:return: Nothing :return: Nothing
""" """
if host is not None:
# we want to track if there are any
# vhosts on the Router instance so that we can
# default to the behavior without vhosts
if self.hosts is None:
self.hosts = set(host)
else:
self.hosts.add(host)
uri = host + uri
if uri in self.routes_all: if uri in self.routes_all:
raise RouteExists("Route already registered: {}".format(uri)) raise RouteExists("Route already registered: {}".format(uri))
@ -118,6 +134,25 @@ class Router:
else: else:
self.routes_static[uri] = route self.routes_static[uri] = route
def remove(self, uri, clean_cache=True, host=None):
if host is not None:
uri = host + uri
try:
route = self.routes_all.pop(uri)
except KeyError:
raise RouteDoesNotExist("Route was not registered: {}".format(uri))
if route in self.routes_always_check:
self.routes_always_check.remove(route)
elif url_hash(uri) in self.routes_dynamic \
and route in self.routes_dynamic[url_hash(uri)]:
self.routes_dynamic[url_hash(uri)].remove(route)
else:
self.routes_static.pop(uri)
if clean_cache:
self._get.cache_clear()
def get(self, request): def get(self, request):
""" """
Gets a request handler based on the URL of the request, or raises an Gets a request handler based on the URL of the request, or raises an
@ -126,10 +161,14 @@ class Router:
:param request: Request object :param request: Request object
:return: handler, arguments, keyword arguments :return: handler, arguments, keyword arguments
""" """
return self._get(request.url, request.method) if self.hosts is None:
return self._get(request.url, request.method, '')
else:
return self._get(request.url, request.method,
request.headers.get("Host", ''))
@lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE)
def _get(self, url, method): def _get(self, url, method, host):
""" """
Gets a request handler based on the URL of the request, or raises an Gets a request handler based on the URL of the request, or raises an
error. Internal method for caching. error. Internal method for caching.
@ -137,6 +176,7 @@ class Router:
:param method: Request method :param method: Request method
:return: handler, arguments, keyword arguments :return: handler, arguments, keyword arguments
""" """
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)
if route: if route:

View File

@ -4,21 +4,24 @@ from functools import partial
from inspect import isawaitable, stack, getmodulename from inspect import isawaitable, stack, getmodulename
from multiprocessing import Process, Event from multiprocessing import Process, Event
from signal import signal, SIGTERM, SIGINT from signal import signal, SIGTERM, SIGINT
from time import sleep
from traceback import format_exc from traceback import format_exc
import logging
from .config import Config from .config import Config
from .exceptions import Handler from .exceptions import Handler
from .log import log, logging from .log import log
from .response import HTTPResponse from .response import HTTPResponse
from .router import Router from .router import Router
from .server import serve from .server import serve, HttpProtocol
from .static import register as static_register from .static import register as static_register
from .exceptions import ServerError from .exceptions import ServerError
from socket import socket, SOL_SOCKET, SO_REUSEADDR
from os import set_inheritable
class Sanic: class Sanic:
def __init__(self, name=None, router=None, error_handler=None): def __init__(self, name=None, router=None,
error_handler=None):
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])
@ -32,6 +35,8 @@ class Sanic:
self._blueprint_order = [] self._blueprint_order = []
self.loop = None self.loop = None
self.debug = None self.debug = None
self.sock = None
self.processes = None
# Register alternative method names # Register alternative method names
self.go_fast = self.run self.go_fast = self.run
@ -41,7 +46,7 @@ class Sanic:
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# Decorator # Decorator
def route(self, uri, methods=None): def route(self, uri, methods=None, host=None):
""" """
Decorates a function to be registered as a route Decorates a function to be registered as a route
@ -56,12 +61,13 @@ class Sanic:
uri = '/' + uri uri = '/' + uri
def response(handler): def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler) self.router.add(uri=uri, methods=methods, handler=handler,
host=host)
return handler return handler
return response return response
def add_route(self, handler, uri, methods=None): def add_route(self, handler, uri, methods=None, host=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
@ -72,9 +78,12 @@ class Sanic:
:param methods: list or tuple of methods allowed :param methods: list or tuple of methods allowed
:return: function or class instance :return: function or class instance
""" """
self.route(uri=uri, methods=methods)(handler) self.route(uri=uri, methods=methods, host=host)(handler)
return handler return handler
def remove_route(self, uri, clean_cache=True, host=None):
self.router.remove(uri, clean_cache, host)
# Decorator # Decorator
def exception(self, *exceptions): def exception(self, *exceptions):
""" """
@ -144,7 +153,8 @@ class Sanic:
def register_blueprint(self, *args, **kwargs): def register_blueprint(self, *args, **kwargs):
# TODO: deprecate 1.0 # TODO: deprecate 1.0
log.warning("Use of register_blueprint will be deprecated in " log.warning("Use of register_blueprint will be deprecated in "
"version 1.0. Please use the blueprint method instead") "version 1.0. Please use the blueprint method instead",
DeprecationWarning)
return self.blueprint(*args, **kwargs) return self.blueprint(*args, **kwargs)
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
@ -237,7 +247,8 @@ class Sanic:
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
after_start=None, before_stop=None, after_stop=None, sock=None, after_start=None, before_stop=None, after_stop=None, sock=None,
workers=1, loop=None): workers=1, loop=None, protocol=HttpProtocol, backlog=100,
stop_event=None, logger=None):
""" """
Runs the HTTP Server and listens until keyboard interrupt or term Runs the HTTP Server and listens until keyboard interrupt or term
signal. On termination, drains connections before closing. signal. On termination, drains connections before closing.
@ -245,25 +256,32 @@ class Sanic:
:param host: Address to host on :param host: Address to host on
:param port: Port to host on :param port: Port to host on
:param debug: Enables debug output (slows server) :param debug: Enables debug output (slows server)
:param before_start: Function to be executed before the server starts :param before_start: Functions to be executed before the server starts
accepting connections accepting connections
:param after_start: Function to be executed after the server starts :param after_start: Functions to be executed after the server starts
accepting connections accepting connections
:param before_stop: Function to be executed when a stop signal is :param before_stop: Functions to be executed when a stop signal is
received before it is respected received before it is respected
:param after_stop: Function to be executed when all requests are :param after_stop: Functions to be executed when all requests are
complete complete
:param sock: Socket for the server to accept connections from :param sock: Socket for the server to accept connections from
:param workers: Number of processes :param workers: Number of processes
received before it is respected received before it is respected
:param loop: asyncio compatible event loop :param loop: asyncio compatible event loop
:param protocol: Subclass of asyncio protocol class
:return: Nothing :return: Nothing
""" """
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s: %(levelname)s: %(message)s"
)
self.error_handler.debug = True self.error_handler.debug = True
self.debug = debug self.debug = debug
self.loop = loop self.loop = loop
server_settings = { server_settings = {
'protocol': protocol,
'host': host, 'host': host,
'port': port, 'port': port,
'sock': sock, 'sock': sock,
@ -272,7 +290,8 @@ class Sanic:
'error_handler': self.error_handler, 'error_handler': self.error_handler,
'request_timeout': self.config.REQUEST_TIMEOUT, 'request_timeout': self.config.REQUEST_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE, 'request_max_size': self.config.REQUEST_MAX_SIZE,
'loop': loop 'loop': loop,
'backlog': backlog
} }
# -------------------------------------------- # # -------------------------------------------- #
@ -289,7 +308,7 @@ class Sanic:
for blueprint in self.blueprints.values(): for blueprint in self.blueprints.values():
listeners += blueprint.listeners[event_name] listeners += blueprint.listeners[event_name]
if args: if args:
if type(args) is not list: if callable(args):
args = [args] args = [args]
listeners += args listeners += args
if reverse: if reverse:
@ -311,7 +330,7 @@ class Sanic:
else: else:
log.info('Spinning up {} workers...'.format(workers)) log.info('Spinning up {} workers...'.format(workers))
self.serve_multiple(server_settings, workers) self.serve_multiple(server_settings, workers, stop_event)
except Exception as e: except Exception as e:
log.exception( log.exception(
@ -323,10 +342,13 @@ class Sanic:
""" """
This kills the Sanic This kills the Sanic
""" """
if self.processes is not None:
for process in self.processes:
process.terminate()
self.sock.close()
get_event_loop().stop() get_event_loop().stop()
@staticmethod def serve_multiple(self, server_settings, workers, stop_event=None):
def serve_multiple(server_settings, workers, stop_event=None):
""" """
Starts multiple server processes simultaneously. Stops on interrupt Starts multiple server processes simultaneously. Stops on interrupt
and terminate signals, and drains connections when complete. and terminate signals, and drains connections when complete.
@ -336,29 +358,37 @@ class Sanic:
:param stop_event: if provided, is used as a stop signal :param stop_event: if provided, is used as a stop signal
:return: :return:
""" """
# In case this is called directly, we configure logging here too.
# This won't interfere with the same call from run()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s: %(levelname)s: %(message)s"
)
server_settings['reuse_port'] = True server_settings['reuse_port'] = True
# Create a stop event to be triggered by a signal # Create a stop event to be triggered by a signal
if not stop_event: if stop_event is None:
stop_event = Event() stop_event = Event()
signal(SIGINT, lambda s, f: stop_event.set()) signal(SIGINT, lambda s, f: stop_event.set())
signal(SIGTERM, lambda s, f: stop_event.set()) signal(SIGTERM, lambda s, f: stop_event.set())
processes = [] self.sock = socket()
self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
self.sock.bind((server_settings['host'], server_settings['port']))
set_inheritable(self.sock.fileno(), True)
server_settings['sock'] = self.sock
server_settings['host'] = None
server_settings['port'] = None
self.processes = []
for _ in range(workers): for _ in range(workers):
process = Process(target=serve, kwargs=server_settings) process = Process(target=serve, kwargs=server_settings)
process.daemon = True
process.start() process.start()
processes.append(process) self.processes.append(process)
# Infinitely wait for the stop event for process in self.processes:
try:
while not stop_event.is_set():
sleep(0.3)
except:
pass
log.info('Spinning down workers...')
for process in processes:
process.terminate()
for process in processes:
process.join() process.join()
# the above processes will block this until they're stopped
self.stop()

View File

@ -1,7 +1,7 @@
import asyncio import asyncio
import traceback
from functools import partial from functools import partial
from inspect import isawaitable from inspect import isawaitable
from multidict import CIMultiDict
from signal import SIGINT, SIGTERM from signal import SIGINT, SIGTERM
from time import time from time import time
from httptools import HttpRequestParser from httptools import HttpRequestParser
@ -18,11 +18,30 @@ from .request import Request
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
current_time = None
class Signal: class Signal:
stopped = False stopped = False
current_time = None class CIDict(dict):
"""
Case Insensitive dict where all keys are converted to lowercase
This does not maintain the inputted case when calling items() or keys()
in favor of speed, since headers are case insensitive
"""
def get(self, key, default=None):
return super().get(key.casefold(), default)
def __getitem__(self, key):
return super().__getitem__(key.casefold())
def __setitem__(self, key, value):
return super().__setitem__(key.casefold(), value)
def __contains__(self, key):
return super().__contains__(key.casefold())
class HttpProtocol(asyncio.Protocol): class HttpProtocol(asyncio.Protocol):
@ -118,18 +137,15 @@ class HttpProtocol(asyncio.Protocol):
exception = PayloadTooLarge('Payload Too Large') exception = PayloadTooLarge('Payload Too Large')
self.write_error(exception) self.write_error(exception)
self.headers.append((name.decode(), value.decode('utf-8'))) self.headers.append((name.decode().casefold(), value.decode()))
def on_headers_complete(self): def on_headers_complete(self):
remote_addr = self.transport.get_extra_info('peername')
if remote_addr:
self.headers.append(('Remote-Addr', '%s:%s' % remote_addr))
self.request = Request( self.request = Request(
url_bytes=self.url, url_bytes=self.url,
headers=CIMultiDict(self.headers), headers=CIDict(self.headers),
version=self.parser.get_http_version(), version=self.parser.get_http_version(),
method=self.parser.get_method().decode() method=self.parser.get_method().decode(),
transport=self.transport
) )
def on_body(self, body): def on_body(self, body):
@ -174,6 +190,12 @@ class HttpProtocol(asyncio.Protocol):
"Writing error failed, connection closed {}".format(e)) "Writing error failed, connection closed {}".format(e))
def bail_out(self, message): def bail_out(self, message):
if self.transport.is_closing():
log.error(
"Connection closed before error was sent to user @ {}".format(
self.transport.get_extra_info('peername')))
log.debug('Error experienced:\n{}'.format(traceback.format_exc()))
else:
exception = ServerError(message) exception = ServerError(message)
self.write_error(exception) self.write_error(exception)
log.error(message) log.error(message)
@ -225,26 +247,33 @@ 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, after_start=None, before_stop=None, after_stop=None, debug=False,
debug=False, request_timeout=60, sock=None, request_timeout=60, sock=None, request_max_size=None,
request_max_size=None, reuse_port=False, loop=None): reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100):
""" """
Starts asynchronous HTTP Server on an individual process. Starts asynchronous HTTP Server on an individual process.
:param host: Address to host on :param host: Address to host on
:param port: Port to host on :param port: Port to host on
:param request_handler: Sanic request handler with middleware :param request_handler: Sanic request handler with middleware
:param error_handler: Sanic error handler with middleware
:param before_start: Function to be executed before the server starts
listening. Takes single argument `loop`
:param after_start: Function to be executed after the server starts :param after_start: Function to be executed after the server starts
listening. Takes single argument `loop` listening. Takes single argument `loop`
:param before_stop: Function to be executed when a stop signal is :param before_stop: Function to be executed when a stop signal is
received before it is respected. Takes single received before it is respected. Takes single
argumenet `loop` argument `loop`
:param after_stop: Function to be executed when a stop signal is
received after it is respected. Takes single
argument `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 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
:param reuse_port: `True` for multiple workers :param reuse_port: `True` for multiple workers
:param loop: asyncio compatible event loop :param loop: asyncio compatible event loop
:param protocol: Subclass of asyncio protocol class
:return: Nothing :return: Nothing
""" """
loop = loop or async_loop.new_event_loop() loop = loop or async_loop.new_event_loop()
@ -258,7 +287,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
connections = set() connections = set()
signal = Signal() signal = Signal()
server = partial( server = partial(
HttpProtocol, protocol,
loop=loop, loop=loop,
connections=connections, connections=connections,
signal=signal, signal=signal,
@ -273,7 +302,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
host, host,
port, port,
reuse_port=reuse_port, reuse_port=reuse_port,
sock=sock sock=sock,
backlog=backlog
) )
# Instead of pulling time at the end of every request, # Instead of pulling time at the end of every request,

View File

@ -16,15 +16,15 @@ async def local_request(method, uri, cookies=None, *args, **kwargs):
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
loop=None, debug=False, *request_args, loop=None, debug=False, server_kwargs={},
**request_kwargs): *request_args, **request_kwargs):
results = [] results = []
exceptions = [] exceptions = []
if gather_request: if gather_request:
@app.middleware
def _collect_request(request): def _collect_request(request):
results.append(request) results.append(request)
app.request_middleware.appendleft(_collect_request)
async def _collect_response(sanic, loop): async def _collect_response(sanic, loop):
try: try:
@ -35,8 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
exceptions.append(e) exceptions.append(e)
app.stop() app.stop()
app.run(host=HOST, debug=debug, port=42101, app.run(host=HOST, debug=debug, port=PORT,
after_start=_collect_response, loop=loop) after_start=_collect_response, loop=loop, **server_kwargs)
if exceptions: if exceptions:
raise ValueError("Exception during request: {}".format(exceptions)) raise ValueError("Exception during request: {}".format(exceptions))

View File

@ -10,7 +10,7 @@ class HTTPMethodView:
.. code-block:: python .. code-block:: python
class DummyView(View): class DummyView(HTTPMethodView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return text('I am get method') return text('I am get method')
def put(self, request, *args, **kwargs): def put(self, request, *args, **kwargs):
@ -25,19 +25,43 @@ class HTTPMethodView:
.. code-block:: python .. code-block:: python
class DummyView(View): class DummyView(HTTPMethodView):
def get(self, request, my_param_here, *args, **kwargs): def get(self, request, my_param_here, *args, **kwargs):
return text('I am get method with %s' % my_param_here) return text('I am get method with %s' % my_param_here)
To add the view into the routing you could use To add the view into the routing you could use
1) app.add_route(DummyView(), '/') 1) app.add_route(DummyView.as_view(), '/')
2) app.route('/')(DummyView()) 2) app.route('/')(DummyView.as_view())
To add any decorator you could set it into decorators variable
""" """
def __call__(self, request, *args, **kwargs): decorators = []
def dispatch_request(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None) handler = getattr(self, request.method.lower(), None)
if handler: if handler:
return handler(request, *args, **kwargs) return handler(request, *args, **kwargs)
raise InvalidUsage( raise InvalidUsage(
'Method {} not allowed for URL {}'.format( 'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405) request.method, request.url), status_code=405)
@classmethod
def as_view(cls, *class_args, **class_kwargs):
""" Converts the class into an actual view function that can be used
with the routing system.
"""
def view(*args, **kwargs):
self = view.view_class(*class_args, **class_kwargs)
return self.dispatch_request(*args, **kwargs)
if cls.decorators:
view.__module__ = cls.__module__
for decorator in cls.decorators:
view = decorator(view)
view.view_class = cls
view.__doc__ = cls.__doc__
view.__module__ = cls.__module__
return view

View File

@ -59,6 +59,71 @@ def test_several_bp_with_url_prefix():
request, response = sanic_endpoint_test(app, uri='/test2/') request, response = sanic_endpoint_test(app, uri='/test2/')
assert response.text == 'Hello2' assert response.text == 'Hello2'
def test_bp_with_host():
app = Sanic('test_bp_host')
bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com")
@bp.route('/')
def handler(request):
return text('Hello')
@bp.route('/', host="sub.example.com")
def handler(request):
return text('Hello subdomain!')
app.blueprint(bp)
headers = {"Host": "example.com"}
request, response = sanic_endpoint_test(app, uri='/test1/',
headers=headers)
assert response.text == 'Hello'
headers = {"Host": "sub.example.com"}
request, response = sanic_endpoint_test(app, uri='/test1/',
headers=headers)
assert response.text == 'Hello subdomain!'
def test_several_bp_with_host():
app = Sanic('test_text')
bp = Blueprint('test_text',
url_prefix='/test',
host="example.com")
bp2 = Blueprint('test_text2',
url_prefix='/test',
host="sub.example.com")
@bp.route('/')
def handler(request):
return text('Hello')
@bp2.route('/')
def handler2(request):
return text('Hello2')
@bp2.route('/other/')
def handler2(request):
return text('Hello3')
app.blueprint(bp)
app.blueprint(bp2)
assert bp.host == "example.com"
headers = {"Host": "example.com"}
request, response = sanic_endpoint_test(app, uri='/test/',
headers=headers)
assert response.text == 'Hello'
assert bp2.host == "sub.example.com"
headers = {"Host": "sub.example.com"}
request, response = sanic_endpoint_test(app, uri='/test/',
headers=headers)
assert response.text == 'Hello2'
request, response = sanic_endpoint_test(app, uri='/test/other/',
headers=headers)
assert response.text == 'Hello3'
def test_bp_middleware(): def test_bp_middleware():
app = Sanic('test_middleware') app = Sanic('test_middleware')

View File

@ -0,0 +1,32 @@
from sanic import Sanic
from sanic.server import HttpProtocol
from sanic.response import text
from sanic.utils import sanic_endpoint_test
app = Sanic('test_custom_porotocol')
class CustomHttpProtocol(HttpProtocol):
def write_response(self, response):
if isinstance(response, str):
response = text(response)
self.transport.write(
response.output(self.request.version)
)
self.transport.close()
@app.route('/1')
async def handler_1(request):
return 'OK'
def test_use_custom_protocol():
server_kwargs = {
'protocol': CustomHttpProtocol
}
request, response = sanic_endpoint_test(app, uri='/1',
server_kwargs=server_kwargs)
assert response.status == 200
assert response.text == 'OK'

View File

@ -1,51 +1,93 @@
import pytest
from bs4 import BeautifulSoup
from sanic import Sanic from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.exceptions import InvalidUsage, ServerError, NotFound from sanic.exceptions import InvalidUsage, ServerError, NotFound
from sanic.utils import sanic_endpoint_test from sanic.utils import sanic_endpoint_test
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
exception_app = Sanic('test_exceptions') class SanicExceptionTestException(Exception):
pass
@exception_app.route('/') @pytest.fixture(scope='module')
def handler(request): def exception_app():
app = Sanic('test_exceptions')
@app.route('/')
def handler(request):
return text('OK') return text('OK')
@app.route('/error')
@exception_app.route('/error') def handler_error(request):
def handler_error(request):
raise ServerError("OK") raise ServerError("OK")
@app.route('/404')
@exception_app.route('/404') def handler_404(request):
def handler_404(request):
raise NotFound("OK") raise NotFound("OK")
@app.route('/invalid')
@exception_app.route('/invalid') def handler_invalid(request):
def handler_invalid(request):
raise InvalidUsage("OK") raise InvalidUsage("OK")
@app.route('/divide_by_zero')
def handle_unhandled_exception(request):
1 / 0
def test_no_exception(): @app.route('/error_in_error_handler_handler')
def custom_error_handler(request):
raise SanicExceptionTestException('Dummy message!')
@app.exception(SanicExceptionTestException)
def error_in_error_handler_handler(request, exception):
1 / 0
return app
def test_no_exception(exception_app):
"""Test that a route works without an exception"""
request, response = sanic_endpoint_test(exception_app) request, response = sanic_endpoint_test(exception_app)
assert response.status == 200 assert response.status == 200
assert response.text == 'OK' assert response.text == 'OK'
def test_server_error_exception(): def test_server_error_exception(exception_app):
"""Test the built-in ServerError exception works"""
request, response = sanic_endpoint_test(exception_app, uri='/error') request, response = sanic_endpoint_test(exception_app, uri='/error')
assert response.status == 500 assert response.status == 500
def test_invalid_usage_exception(): def test_invalid_usage_exception(exception_app):
"""Test the built-in InvalidUsage exception works"""
request, response = sanic_endpoint_test(exception_app, uri='/invalid') request, response = sanic_endpoint_test(exception_app, uri='/invalid')
assert response.status == 400 assert response.status == 400
def test_not_found_exception(): def test_not_found_exception(exception_app):
"""Test the built-in NotFound exception works"""
request, response = sanic_endpoint_test(exception_app, uri='/404') request, response = sanic_endpoint_test(exception_app, uri='/404')
assert response.status == 404 assert response.status == 404
def test_handled_unhandled_exception(exception_app):
"""Test that an exception not built into sanic is handled"""
request, response = sanic_endpoint_test(
exception_app, uri='/divide_by_zero')
assert response.status == 500
soup = BeautifulSoup(response.body, 'html.parser')
assert soup.h1.text == 'Internal Server Error'
message = " ".join(soup.p.text.split())
assert message == (
"The server encountered an internal error and "
"cannot complete your request.")
def test_exception_in_exception_handler(exception_app):
"""Test that an exception thrown in an error handler is handled"""
request, response = sanic_endpoint_test(
exception_app, uri='/error_in_error_handler_handler')
assert response.status == 500
assert response.body == b'An error occurred while handling an error'

View File

@ -2,6 +2,7 @@ from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.exceptions import InvalidUsage, ServerError, NotFound from sanic.exceptions import InvalidUsage, ServerError, NotFound
from sanic.utils import sanic_endpoint_test from sanic.utils import sanic_endpoint_test
from bs4 import BeautifulSoup
exception_handler_app = Sanic('test_exception_handler') exception_handler_app = Sanic('test_exception_handler')
@ -21,6 +22,12 @@ def handler_3(request):
raise NotFound("OK") raise NotFound("OK")
@exception_handler_app.route('/4')
def handler_4(request):
foo = bar
return text(foo)
@exception_handler_app.exception(NotFound, ServerError) @exception_handler_app.exception(NotFound, ServerError)
def handler_exception(request, exception): def handler_exception(request, exception):
return text("OK") return text("OK")
@ -47,3 +54,20 @@ def test_text_exception__handler():
exception_handler_app, uri='/random') exception_handler_app, uri='/random')
assert response.status == 200 assert response.status == 200
assert response.text == 'OK' assert response.text == 'OK'
def test_html_traceback_output_in_debug_mode():
request, response = sanic_endpoint_test(
exception_handler_app, uri='/4', debug=True)
assert response.status == 500
soup = BeautifulSoup(response.body, 'html.parser')
html = str(soup)
assert 'response = handler(request, *args, **kwargs)' in html
assert 'handler_4' in html
assert 'foo = bar' in html
summary_text = " ".join(soup.select('.summary')[0].text.split())
assert (
"NameError: name 'bar' "
"is not defined while handling uri /4") == summary_text

33
tests/test_logging.py Normal file
View File

@ -0,0 +1,33 @@
import asyncio
from sanic.response import text
from sanic import Sanic
from io import StringIO
from sanic.utils import sanic_endpoint_test
import logging
logging_format = '''module: %(module)s; \
function: %(funcName)s(); \
message: %(message)s'''
def test_log():
log_stream = StringIO()
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
logging.basicConfig(
format=logging_format,
level=logging.DEBUG,
stream=log_stream
)
log = logging.getLogger()
app = Sanic('test_logging')
@app.route('/')
def handler(request):
log.info('hello world')
return text('hello')
request, response = sanic_endpoint_test(app)
log_text = log_stream.getvalue().strip().split('\n')[-3]
assert log_text == "module: test_logging; function: handler(); message: hello world"
if __name__ =="__main__":
test_log()

View File

@ -1,7 +1,9 @@
from multiprocessing import Array, Event, Process from multiprocessing import Array, Event, Process
from time import sleep from time import sleep, time
from ujson import loads as json_loads from ujson import loads as json_loads
import pytest
from sanic import Sanic from sanic import Sanic
from sanic.response import json from sanic.response import json
from sanic.utils import local_request, HOST, PORT from sanic.utils import local_request, HOST, PORT
@ -13,8 +15,9 @@ from sanic.utils import local_request, HOST, PORT
# TODO: Figure out why this freezes on pytest but not when # TODO: Figure out why this freezes on pytest but not when
# executed via interpreter # executed via interpreter
@pytest.mark.skip(
def skip_test_multiprocessing(): reason="Freezes with pytest not on interpreter")
def test_multiprocessing():
app = Sanic('test_json') app = Sanic('test_json')
response = Array('c', 50) response = Array('c', 50)
@ -51,3 +54,28 @@ def skip_test_multiprocessing():
raise ValueError("Expected JSON response but got '{}'".format(response)) raise ValueError("Expected JSON response but got '{}'".format(response))
assert results.get('test') == True assert results.get('test') == True
@pytest.mark.skip(
reason="Freezes with pytest not on interpreter")
def test_drain_connections():
app = Sanic('test_json')
@app.route('/')
async def handler(request):
return json({"test": True})
stop_event = Event()
async def after_start(*args, **kwargs):
http_response = await local_request('get', '/')
stop_event.set()
start = time()
app.serve_multiple({
'host': HOST,
'port': PORT,
'after_start': after_start,
'request_handler': app.handle_request,
}, workers=2, stop_event=stop_event)
end = time()
assert end - start < 0.05

View File

@ -1,9 +1,10 @@
from json import loads as json_loads, dumps as json_dumps from json import loads as json_loads, dumps as json_dumps
from sanic import Sanic from sanic import Sanic
from sanic.response import json, text from sanic.response import json, text, redirect
from sanic.utils import sanic_endpoint_test from sanic.utils import sanic_endpoint_test
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
import pytest
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
# GET # GET
@ -33,6 +34,31 @@ def test_text():
assert response.text == 'Hello' assert response.text == 'Hello'
def test_headers():
app = Sanic('test_text')
@app.route('/')
async def handler(request):
headers = {"spam": "great"}
return text('Hello', headers=headers)
request, response = sanic_endpoint_test(app)
assert response.headers.get('spam') == 'great'
def test_non_str_headers():
app = Sanic('test_text')
@app.route('/')
async def handler(request):
headers = {"answer": 42}
return text('Hello', headers=headers)
request, response = sanic_endpoint_test(app)
assert response.headers.get('answer') == '42'
def test_invalid_response(): def test_invalid_response():
app = Sanic('test_invalid_response') app = Sanic('test_invalid_response')
@ -92,6 +118,24 @@ def test_query_string():
assert request.args.get('test2') == 'false' assert request.args.get('test2') == 'false'
def test_token():
app = Sanic('test_post_token')
@app.route('/')
async def handler(request):
return text('OK')
# uuid4 generated token.
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
headers = {
'content-type': 'application/json',
'Authorization': 'Token {}'.format(token)
}
request, response = sanic_endpoint_test(app, headers=headers)
assert request.token == token
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
# POST # POST
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
@ -145,3 +189,73 @@ def test_post_form_multipart_form_data():
request, response = sanic_endpoint_test(app, data=payload, headers=headers) request, response = sanic_endpoint_test(app, data=payload, headers=headers)
assert request.form.get('test') == 'OK' assert request.form.get('test') == 'OK'
@pytest.fixture
def redirect_app():
app = Sanic('test_redirection')
@app.route('/redirect_init')
async def redirect_init(request):
return redirect("/redirect_target")
@app.route('/redirect_init_with_301')
async def redirect_init_with_301(request):
return redirect("/redirect_target", status=301)
@app.route('/redirect_target')
async def redirect_target(request):
return text('OK')
return app
def test_redirect_default_302(redirect_app):
"""
We expect a 302 default status code and the headers to be set.
"""
request, response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init",
allow_redirects=False)
assert response.status == 302
assert response.headers["Location"] == "/redirect_target"
assert response.headers["Content-Type"] == 'text/html; charset=utf-8'
def test_redirect_headers_none(redirect_app):
request, response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init",
headers=None,
allow_redirects=False)
assert response.status == 302
assert response.headers["Location"] == "/redirect_target"
def test_redirect_with_301(redirect_app):
"""
Test redirection with a different status code.
"""
request, response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init_with_301",
allow_redirects=False)
assert response.status == 301
assert response.headers["Location"] == "/redirect_target"
def test_get_then_redirect_follow_redirect(redirect_app):
"""
With `allow_redirects` we expect a 200.
"""
response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init", gather_request=False,
allow_redirects=True)
assert response.status == 200
assert response.text == 'OK'

View File

@ -2,7 +2,7 @@ import pytest
from sanic import Sanic from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.router import RouteExists from sanic.router import RouteExists, RouteDoesNotExist
from sanic.utils import sanic_endpoint_test from sanic.utils import sanic_endpoint_test
@ -356,3 +356,110 @@ def test_add_route_method_not_allowed():
request, response = sanic_endpoint_test(app, method='post', uri='/test') request, response = sanic_endpoint_test(app, method='post', uri='/test')
assert response.status == 405 assert response.status == 405
def test_remove_static_route():
app = Sanic('test_remove_static_route')
async def handler1(request):
return text('OK1')
async def handler2(request):
return text('OK2')
app.add_route(handler1, '/test')
app.add_route(handler2, '/test2')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/test2')
assert response.status == 200
app.remove_route('/test')
app.remove_route('/test2')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/test2')
assert response.status == 404
def test_remove_dynamic_route():
app = Sanic('test_remove_dynamic_route')
async def handler(request, name):
return text('OK')
app.add_route(handler, '/folder/<name>')
request, response = sanic_endpoint_test(app, uri='/folder/test123')
assert response.status == 200
app.remove_route('/folder/<name>')
request, response = sanic_endpoint_test(app, uri='/folder/test123')
assert response.status == 404
def test_remove_inexistent_route():
app = Sanic('test_remove_inexistent_route')
with pytest.raises(RouteDoesNotExist):
app.remove_route('/test')
def test_remove_unhashable_route():
app = Sanic('test_remove_unhashable_route')
async def handler(request, unhashable):
return text('OK')
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/')
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
assert response.status == 200
app.remove_route('/folder/<unhashable:[A-Za-z0-9/]+>/end/')
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
assert response.status == 404
def test_remove_route_without_clean_cache():
app = Sanic('test_remove_static_route')
async def handler(request):
return text('OK')
app.add_route(handler, '/test')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
app.remove_route('/test', clean_cache=True)
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 404
app.add_route(handler, '/test')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
app.remove_route('/test', clean_cache=False)
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200

View File

@ -0,0 +1,59 @@
from io import StringIO
from random import choice
from string import ascii_letters
import signal
import pytest
from sanic import Sanic
AVAILABLE_LISTENERS = [
'before_start',
'after_start',
'before_stop',
'after_stop'
]
def create_listener(listener_name, in_list):
async def _listener(app, loop):
print('DEBUG MESSAGE FOR PYTEST for {}'.format(listener_name))
in_list.insert(0, app.name + listener_name)
return _listener
def start_stop_app(random_name_app, **run_kwargs):
def stop_on_alarm(signum, frame):
raise KeyboardInterrupt('SIGINT for sanic to stop gracefully')
signal.signal(signal.SIGALRM, stop_on_alarm)
signal.alarm(1)
try:
random_name_app.run(**run_kwargs)
except KeyboardInterrupt:
pass
@pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS)
def test_single_listener(listener_name):
"""Test that listeners on their own work"""
random_name_app = Sanic(''.join(
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
output = list()
start_stop_app(
random_name_app,
**{listener_name: create_listener(listener_name, output)})
assert random_name_app.name + listener_name == output.pop()
def test_all_listeners():
random_name_app = Sanic(''.join(
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
output = list()
start_stop_app(
random_name_app,
**{listener_name: create_listener(listener_name, output)
for listener_name in AVAILABLE_LISTENERS})
for listener_name in AVAILABLE_LISTENERS:
assert random_name_app.name + listener_name == output.pop()

23
tests/test_vhosts.py Normal file
View File

@ -0,0 +1,23 @@
from sanic import Sanic
from sanic.response import json, text
from sanic.utils import sanic_endpoint_test
def test_vhosts():
app = Sanic('test_text')
@app.route('/', host="example.com")
async def handler(request):
return text("You're at example.com!")
@app.route('/', host="subdomain.example.com")
async def handler(request):
return text("You're at subdomain.example.com!")
headers = {"Host": "example.com"}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == "You're at example.com!"
headers = {"Host": "subdomain.example.com"}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == "You're at subdomain.example.com!"

View File

@ -26,7 +26,7 @@ def test_methods():
def delete(self, request): def delete(self, request):
return text('I am delete method') return text('I am delete method')
app.add_route(DummyView(), '/') app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get") request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method' assert response.text == 'I am get method'
@ -48,7 +48,7 @@ def test_unexisting_methods():
def get(self, request): def get(self, request):
return text('I am get method') return text('I am get method')
app.add_route(DummyView(), '/') app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get") request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method' assert response.text == 'I am get method'
request, response = sanic_endpoint_test(app, method="post") request, response = sanic_endpoint_test(app, method="post")
@ -63,7 +63,7 @@ def test_argument_methods():
def get(self, request, my_param_here): def get(self, request, my_param_here):
return text('I am get method with %s' % my_param_here) return text('I am get method with %s' % my_param_here)
app.add_route(DummyView(), '/<my_param_here>') app.add_route(DummyView.as_view(), '/<my_param_here>')
request, response = sanic_endpoint_test(app, uri='/test123') request, response = sanic_endpoint_test(app, uri='/test123')
@ -79,7 +79,7 @@ def test_with_bp():
def get(self, request): def get(self, request):
return text('I am get method') return text('I am get method')
bp.add_route(DummyView(), '/') bp.add_route(DummyView.as_view(), '/')
app.blueprint(bp) app.blueprint(bp)
request, response = sanic_endpoint_test(app) request, response = sanic_endpoint_test(app)
@ -96,7 +96,7 @@ def test_with_bp_with_url_prefix():
def get(self, request): def get(self, request):
return text('I am get method') return text('I am get method')
bp.add_route(DummyView(), '/') bp.add_route(DummyView.as_view(), '/')
app.blueprint(bp) app.blueprint(bp)
request, response = sanic_endpoint_test(app, uri='/test1/') request, response = sanic_endpoint_test(app, uri='/test1/')
@ -112,7 +112,7 @@ def test_with_middleware():
def get(self, request): def get(self, request):
return text('I am get method') return text('I am get method')
app.add_route(DummyView(), '/') app.add_route(DummyView.as_view(), '/')
results = [] results = []
@ -145,7 +145,7 @@ def test_with_middleware_response():
def get(self, request): def get(self, request):
return text('I am get method') return text('I am get method')
app.add_route(DummyView(), '/') app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app) request, response = sanic_endpoint_test(app)
@ -153,3 +153,44 @@ def test_with_middleware_response():
assert type(results[0]) is Request assert type(results[0]) is Request
assert type(results[1]) is Request assert type(results[1]) is Request
assert issubclass(type(results[2]), HTTPResponse) assert issubclass(type(results[2]), HTTPResponse)
def test_with_custom_class_methods():
app = Sanic('test_with_custom_class_methods')
class DummyView(HTTPMethodView):
global_var = 0
def _iternal_method(self):
self.global_var += 10
def get(self, request):
self._iternal_method()
return text('I am get method and global var is {}'.format(self.global_var))
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method and global var is 10'
def test_with_decorator():
app = Sanic('test_with_decorator')
results = []
def stupid_decorator(view):
def decorator(*args, **kwargs):
results.append(1)
return view(*args, **kwargs)
return decorator
class DummyView(HTTPMethodView):
decorators = [stupid_decorator]
def get(self, request):
return text('I am get method')
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method'
assert results[0] == 1

26
tox.ini
View File

@ -1,22 +1,22 @@
[tox] [tox]
envlist = py35, py36 envlist = py35, py36, flake8
[travis]
python =
3.5: py35, flake8
3.6: py36, flake8
[testenv] [testenv]
deps = deps =
aiohttp aiohttp
pytest pytest
coverage beautifulsoup4
commands = commands =
coverage run -m pytest -v tests {posargs} pytest tests {posargs}
mv .coverage .coverage.{envname}
whitelist_externals =
coverage
mv
echo
[testenv:flake8] [testenv:flake8]
deps = deps =
@ -24,11 +24,3 @@ deps =
commands = commands =
flake8 sanic flake8 sanic
[testenv:report]
commands =
coverage combine
coverage report
coverage html
echo "Open file://{toxinidir}/coverage/index.html"