Merge branch 'master' into improved_config

This commit is contained in:
Tim Mundt 2017-01-13 12:34:56 +01:00
commit 0b9094d348
29 changed files with 870 additions and 145 deletions

4
.gitignore vendored
View File

@ -1,11 +1,13 @@
*~ *~
*.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

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

@ -60,6 +60,7 @@ if __name__ == "__main__":
* [Cookies](docs/cookies.md) * [Cookies](docs/cookies.md)
* [Static Files](docs/static_files.md) * [Static Files](docs/static_files.md)
* [Configuration](docs/config.md) * [Configuration](docs/config.md)
* [Custom Protocol](docs/custom_protocol.md)
* [Testing](docs/testing.md) * [Testing](docs/testing.md)
* [Deploying](docs/deploying.md) * [Deploying](docs/deploying.md)
* [Contributing](docs/contributing.md) * [Contributing](docs/contributing.md)

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)
```

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,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

@ -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,4 +1,5 @@
from .response import text from .response import text
from .log import log
from traceback import format_exc from traceback import format_exc
@ -56,18 +57,31 @@ 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( response_message = (
"Error: {}\nException: {}".format( 'Exception occurred while handling uri: "{}"\n{}'.format(
exception, format_exc()), status=500) request.url, format_exc()))
log.error(response_message)
return text(response_message, status=500)
else: else:
return text( return text(
"An error occurred while generating the request", status=500) 'An error occurred while generating the response', status=500)

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
@ -64,7 +67,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 +75,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:

View File

@ -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

View File

@ -24,16 +24,20 @@ 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
Usage: Usage:
@sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...]) @app.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
@sanic.route('/my/url/<my_paramter>:type', methods['GET', 'POST', ...]) @app.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
@ -52,8 +56,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
:param uri: Path to match :param uri: Path to match
@ -63,6 +68,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))
@ -110,6 +126,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
@ -117,10 +152,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=ROUTER_CACHE_SIZE) @lru_cache(maxsize=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.
@ -128,6 +167,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,7 +4,6 @@ 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 import logging
@ -13,9 +12,11 @@ from .exceptions import Handler
from .log import log 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:
@ -39,6 +40,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
@ -48,7 +51,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
:param uri: path of the URL :param uri: path of the URL
@ -62,12 +65,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
@ -77,9 +81,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):
""" """
@ -239,25 +246,27 @@ 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):
""" """
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.
: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
""" """
self.error_handler.debug = True self.error_handler.debug = True
@ -265,6 +274,7 @@ class Sanic:
self.loop = loop self.loop = loop
server_settings = { server_settings = {
'protocol': protocol,
'host': host, 'host': host,
'port': port, 'port': port,
'sock': sock, 'sock': sock,
@ -273,7 +283,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
} }
# -------------------------------------------- # # -------------------------------------------- #
@ -290,7 +301,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:
@ -312,7 +323,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(
@ -324,10 +335,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.
@ -339,26 +353,28 @@ class Sanic:
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

@ -224,24 +224,30 @@ 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 argumenet `loop` received before it is respected. Takes single argumenet `loop`
:param after_stop: Function to be executed when a stop signal is
received after it is respected. Takes single argumenet `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()
@ -255,7 +261,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,
@ -270,7 +276,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

@ -7,7 +7,7 @@ class HTTPMethodView:
to every HTTP method you want to support. to every HTTP method you want to support.
For example: For example:
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')
@ -20,20 +20,44 @@ class HTTPMethodView:
405 response. 405 response.
If you need any url params just mention them in method definition: If you need any url params just mention them in method definition:
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,86 @@
import pytest
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 exception_app():
app = Sanic('test_exceptions')
@app.route('/')
def handler(request): 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
assert response.body == b'An error occurred while generating the response'
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

@ -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

@ -33,6 +33,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 +117,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
# ------------------------------------------------------------ # # ------------------------------------------------------------ #

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

25
tox.ini
View File

@ -1,22 +1,21 @@
[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
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 +23,3 @@ deps =
commands = commands =
flake8 sanic flake8 sanic
[testenv:report]
commands =
coverage combine
coverage report
coverage html
echo "Open file://{toxinidir}/coverage/index.html"