Merge branch 'master' into 420

This commit is contained in:
Raphael Deem 2017-02-15 09:29:52 -08:00 committed by GitHub
commit 1cf1024332
30 changed files with 491 additions and 270 deletions

View File

@ -78,10 +78,7 @@ Documentation
TODO TODO
---- ----
* Streamed file processing * Streamed file processing
* File output * http2
* Examples of integrations with 3rd-party modules
* RESTful router
Limitations Limitations
----------- -----------
* No wheels for uvloop and httptools on Windows :( * No wheels for uvloop and httptools on Windows :(

View File

@ -131,8 +131,8 @@ can be used to implement our API versioning scheme.
from sanic.response import text from sanic.response import text
from sanic import Blueprint from sanic import Blueprint
blueprint_v1 = Blueprint('v1') blueprint_v1 = Blueprint('v1', url_prefix='/v1')
blueprint_v2 = Blueprint('v2') blueprint_v2 = Blueprint('v2', url_prefix='/v2')
@blueprint_v1.route('/') @blueprint_v1.route('/')
async def api_v1_root(request): async def api_v1_root(request):

View File

@ -7,15 +7,6 @@ keyword arguments:
- `host` *(default `"127.0.0.1"`)*: Address to host the server on. - `host` *(default `"127.0.0.1"`)*: Address to host the server on.
- `port` *(default `8000`)*: Port to host the server on. - `port` *(default `8000`)*: Port to host the server on.
- `debug` *(default `False`)*: Enables debug output (slows server). - `debug` *(default `False`)*: Enables debug output (slows server).
- `before_start` *(default `None`)*: Function or list of functions to be executed
before the server starts accepting connections.
- `after_start` *(default `None`)*: Function or list of functions to be executed
after the server starts accepting connections.
- `before_stop` *(default `None`)*: Function or list of functions to be
executed when a stop signal is received before it is
respected.
- `after_stop` *(default `None`)*: Function or list of functions to be executed
when all requests are complete.
- `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s). - `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s).
- `sock` *(default `None`)*: Socket for the server to accept connections from. - `sock` *(default `None`)*: Socket for the server to accept connections from.
- `workers` *(default `1`)*: Number of worker processes to spawn. - `workers` *(default `1`)*: Number of worker processes to spawn.

View File

@ -7,3 +7,4 @@ A list of Sanic extensions created by the community.
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors. - [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template. - [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI. - [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
- [Websockets](https://github.com/r0fls/sanic-websockets): Minimal wrapper to work with [Websockets](https://github.com/aaugustin/websockets)

View File

@ -145,6 +145,28 @@ Other things to keep in mind when using `url_for`:
url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two') url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two')
# /posts/5?arg_one=one&arg_two=two # /posts/5?arg_one=one&arg_two=two
``` ```
- Multivalue argument can be passed to `url_for`. For example:
```python
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'])
# /posts/5?arg_one=one&arg_one=two
```
- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not support now and will be ignored). For example:
```python
url = app.url_for('post_handler', post_id=5, arg_one='one', _anchor='anchor')
# /posts/5?arg_one=one#anchor
url = app.url_for('post_handler', post_id=5, arg_one='one', _external=True)
# //server/posts/5?arg_one=one
# _external requires passed argument _server or SERVER_NAME in app.config or url will be same as no _external
url = app.url_for('post_handler', post_id=5, arg_one='one', _scheme='http', _external=True)
# http://server/posts/5?arg_one=one
# when specifying _scheme, _external must be True
# you can pass all special arguments one time
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2, _anchor='anchor', _scheme='http', _external=True, _server='another_server:8888')
# http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor
```
- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be thrown. - All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be thrown.

View File

@ -1,6 +1,6 @@
""" """
Example of caching using aiocache package. To run it you will need a Redis Example of caching using aiocache package. To run it you will need a Redis
instance running in localhost:6379. instance running in localhost:6379. You can also try with SimpleMemoryCache.
Running this example you will see that the first call lasts 3 seconds and Running this example you will see that the first call lasts 3 seconds and
the rest are instant because the value is retrieved from the Redis. the rest are instant because the value is retrieved from the Redis.
@ -20,9 +20,14 @@ from aiocache.serializers import JsonSerializer
app = Sanic(__name__) app = Sanic(__name__)
aiocache.settings.set_defaults(
class_="aiocache.RedisCache" @app.listener('before_server_start')
) def init_cache(sanic, loop):
aiocache.settings.set_defaults(
class_="aiocache.RedisCache",
# class_="aiocache.SimpleMemoryCache",
loop=loop
)
@cached(key="my_custom_key", serializer=JsonSerializer()) @cached(key="my_custom_key", serializer=JsonSerializer())
@ -38,4 +43,4 @@ async def test(request):
return json(await expensive_call()) return json(await expensive_call())
app.run(host="0.0.0.0", port=8000, loop=asyncio.get_event_loop()) app.run(host="0.0.0.0", port=8000)

View File

@ -8,6 +8,7 @@ app = Sanic(__name__)
sem = None sem = None
@app.listener('before_server_start')
def init(sanic, loop): def init(sanic, loop):
global sem global sem
CONCURRENCY_PER_WORKER = 4 CONCURRENCY_PER_WORKER = 4
@ -33,4 +34,4 @@ async def test(request):
return json(response) return json(response)
app.run(host="0.0.0.0", port=8000, workers=2, before_start=init) app.run(host="0.0.0.0", port=8000, workers=2)

View File

@ -26,6 +26,7 @@ async def get_pool():
app = Sanic(name=__name__) app = Sanic(name=__name__)
@app.listener('before_server_start')
async def prepare_db(app, loop): async def prepare_db(app, loop):
""" """
Let's create some table and add some data Let's create some table and add some data
@ -61,5 +62,4 @@ async def handle(request):
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', app.run(host='0.0.0.0',
port=8000, port=8000,
debug=True, debug=True)
before_start=prepare_db)

View File

@ -32,7 +32,7 @@ polls = sa.Table('sanic_polls', metadata,
app = Sanic(name=__name__) app = Sanic(name=__name__)
@app.listener('before_server_start')
async def prepare_db(app, loop): async def prepare_db(app, loop):
""" Let's add some data """ Let's add some data
@ -58,9 +58,10 @@ async def handle(request):
async with engine.acquire() as conn: async with engine.acquire() as conn:
result = [] result = []
async for row in conn.execute(polls.select()): async for row in conn.execute(polls.select()):
result.append({"question": row.question, "pub_date": row.pub_date}) result.append({"question": row.question,
"pub_date": row.pub_date})
return json({"polls": result}) return json({"polls": result})
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, before_start=prepare_db) app.run(host='0.0.0.0', port=8000)

View File

@ -27,6 +27,7 @@ def jsonify(records):
app = Sanic(__name__) app = Sanic(__name__)
@app.listener('before_server_start')
async def create_db(app, loop): async def create_db(app, loop):
""" """
Create some table and add some data Create some table and add some data
@ -55,4 +56,4 @@ async def handler(request):
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, before_start=create_db) app.run(host='0.0.0.0', port=8000)

View File

@ -14,14 +14,6 @@ from peewee_async import Manager, PostgresqlDatabase
# we instantiate a custom loop so we can pass it to our db manager # we instantiate a custom loop so we can pass it to our db manager
def setup(app, loop):
database = PostgresqlDatabase(database='test',
host='127.0.0.1',
user='postgres',
password='mysecretpassword')
objects = Manager(database, loop=loop)
## from peewee_async docs: ## from peewee_async docs:
# Also theres no need to connect and re-connect before executing async queries # Also theres no need to connect and re-connect before executing async queries
# with manager! Its all automatic. But you can run Manager.connect() or # with manager! Its all automatic. But you can run Manager.connect() or
@ -48,6 +40,15 @@ objects.database.allow_sync = False # this will raise AssertionError on ANY sync
app = Sanic('peewee_example') app = Sanic('peewee_example')
@app.listener('before_server_start')
def setup(app, loop):
database = PostgresqlDatabase(database='test',
host='127.0.0.1',
user='postgres',
password='mysecretpassword')
objects = Manager(database, loop=loop)
@app.route('/post/<key>/<value>') @app.route('/post/<key>/<value>')
async def post(request, key, value): async def post(request, key, value):
""" """
@ -75,4 +76,4 @@ async def get(request):
if __name__ == "__main__": if __name__ == "__main__":
app.run(host='0.0.0.0', port=8000, before_start=setup) app.run(host='0.0.0.0', port=8000)

View File

@ -64,12 +64,14 @@ def query_string(request):
# Run Server # Run Server
# ----------------------------------------------- # # ----------------------------------------------- #
@app.listener('after_server_start')
def after_start(app, loop): def after_start(app, loop):
log.info("OH OH OH OH OHHHHHHHH") log.info("OH OH OH OH OHHHHHHHH")
@app.listener('before_server_stop')
def before_stop(app, loop): def before_stop(app, loop):
log.info("TRIED EVERYTHING") log.info("TRIED EVERYTHING")
app.run(host="0.0.0.0", port=8000, debug=True, after_start=after_start, before_stop=before_stop) app.run(host="0.0.0.0", port=8000, debug=True)

View File

@ -1,5 +1,7 @@
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from sanic.constants import HTTP_METHODS
from sanic.views import CompositionView
FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host']) FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host'])
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host']) FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
@ -11,9 +13,9 @@ FutureStatic = namedtuple('Route',
class Blueprint: class Blueprint:
def __init__(self, name, url_prefix=None, host=None): def __init__(self, name, url_prefix=None, host=None):
""" """Create a new blueprint
Creates a new blueprint
:param name: Unique name of the blueprint :param name: unique name of the blueprint
:param url_prefix: URL to be prefixed before all route URLs :param url_prefix: URL to be prefixed before all route URLs
""" """
self.name = name self.name = name
@ -27,9 +29,7 @@ class Blueprint:
self.statics = [] self.statics = []
def register(self, app, options): def register(self, app, options):
""" """Register the blueprint to the sanic app."""
Registers the blueprint to the sanic app.
"""
url_prefix = options.get('url_prefix', self.url_prefix) url_prefix = options.get('url_prefix', self.url_prefix)
@ -65,11 +65,16 @@ class Blueprint:
app.static(uri, future.file_or_directory, app.static(uri, future.file_or_directory,
*future.args, **future.kwargs) *future.args, **future.kwargs)
# Event listeners
for event, listeners in self.listeners.items():
for listener in listeners:
app.listener(event)(listener)
def route(self, uri, methods=frozenset({'GET'}), host=None): def route(self, uri, methods=frozenset({'GET'}), host=None):
""" """Create a blueprint route from a decorated function.
Creates a blueprint route from a decorated function.
:param uri: Endpoint at which the route will be accessible. :param uri: endpoint at which the route will be accessible.
:param methods: List of acceptable HTTP methods. :param methods: list of acceptable HTTP methods.
""" """
def decorator(handler): def decorator(handler):
route = FutureRoute(handler, uri, methods, host) route = FutureRoute(handler, uri, methods, host)
@ -77,20 +82,33 @@ class Blueprint:
return handler return handler
return decorator return decorator
def add_route(self, handler, uri, methods=None, host=None): def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None):
"""Create a blueprint route from a function.
:param handler: function for handling uri requests. Accepts function,
or class instance with a view_class method.
:param uri: endpoint at which the route will be accessible.
:param methods: list of acceptable HTTP methods.
:return: function or class instance
""" """
Creates a blueprint route from a function. # Handle HTTPMethodView differently
:param handler: Function to handle uri request. if hasattr(handler, 'view_class'):
:param uri: Endpoint at which the route will be accessible. methods = set()
:param methods: List of acceptable HTTP methods.
""" for method in HTTP_METHODS:
route = FutureRoute(handler, uri, methods, host) if getattr(handler.view_class, method.lower(), None):
self.routes.append(route) methods.add(method)
# handle composition view differently
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
self.route(uri=uri, methods=methods, host=host)(handler)
return handler return handler
def listener(self, event): def listener(self, event):
""" """Create a listener from a decorated function.
Create a listener from a decorated function.
:param event: Event to listen to. :param event: Event to listen to.
""" """
def decorator(listener): def decorator(listener):
@ -99,9 +117,7 @@ class Blueprint:
return decorator return decorator
def middleware(self, *args, **kwargs): def middleware(self, *args, **kwargs):
""" """Create a blueprint middleware from a decorated function."""
Creates a blueprint middleware from a decorated function.
"""
def register_middleware(_middleware): def register_middleware(_middleware):
future_middleware = FutureMiddleware(_middleware, args, kwargs) future_middleware = FutureMiddleware(_middleware, args, kwargs)
self.middlewares.append(future_middleware) self.middlewares.append(future_middleware)
@ -116,9 +132,7 @@ class Blueprint:
return register_middleware return register_middleware
def exception(self, *args, **kwargs): def exception(self, *args, **kwargs):
""" """Create a blueprint exception from a decorated function."""
Creates a blueprint exception from a decorated function.
"""
def decorator(handler): def decorator(handler):
exception = FutureException(handler, args, kwargs) exception = FutureException(handler, args, kwargs)
self.exceptions.append(exception) self.exceptions.append(exception)
@ -126,9 +140,9 @@ class Blueprint:
return decorator return decorator
def static(self, uri, file_or_directory, *args, **kwargs): def static(self, uri, file_or_directory, *args, **kwargs):
""" """Create a blueprint static route from a decorated function.
Creates a blueprint static route from a decorated function.
:param uri: Endpoint at which the route will be accessible. :param uri: endpoint at which the route will be accessible.
:param file_or_directory: Static asset. :param file_or_directory: Static asset.
""" """
static = FutureStatic(uri, file_or_directory, args, kwargs) static = FutureStatic(uri, file_or_directory, args, kwargs)

View File

@ -39,8 +39,9 @@ class Config(dict):
self[attr] = value self[attr] = value
def from_envvar(self, variable_name): def from_envvar(self, variable_name):
"""Loads a configuration from an environment variable pointing to """Load a configuration from an environment variable pointing to
a configuration file. a configuration file.
:param variable_name: name of the environment variable :param variable_name: name of the environment variable
:return: bool. ``True`` if able to load config, ``False`` otherwise. :return: bool. ``True`` if able to load config, ``False`` otherwise.
""" """
@ -52,8 +53,9 @@ class Config(dict):
return self.from_pyfile(config_file) return self.from_pyfile(config_file)
def from_pyfile(self, filename): def from_pyfile(self, filename):
"""Updates the values in the config from a Python file. Only the uppercase """Update the values in the config from a Python file.
variables in that module are stored in the config. Only the uppercase variables in that module are stored in the config.
:param filename: an absolute path to the config file :param filename: an absolute path to the config file
""" """
module = types.ModuleType('config') module = types.ModuleType('config')
@ -69,7 +71,7 @@ class Config(dict):
return True return True
def from_object(self, obj): def from_object(self, obj):
"""Updates the values from the given object. """Update the values from the given object.
Objects are usually either modules or classes. Objects are usually either modules or classes.
Just the uppercase variables in that object are stored in the config. Just the uppercase variables in that object are stored in the config.

View File

@ -39,8 +39,7 @@ _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
class CookieJar(dict): class CookieJar(dict):
""" """CookieJar dynamically writes headers as cookies are added and removed
CookieJar dynamically writes headers as cookies are added and removed
It gets around the limitation of one header per name by using the It gets around the limitation of one header per name by using the
MultiHeader class to provide a unique key that encodes to Set-Cookie. MultiHeader class to provide a unique key that encodes to Set-Cookie.
""" """
@ -75,9 +74,7 @@ class CookieJar(dict):
class Cookie(dict): class Cookie(dict):
""" """A stripped down version of Morsel from SimpleCookie #gottagofast"""
This is a stripped down version of Morsel from SimpleCookie #gottagofast
"""
_keys = { _keys = {
"expires": "expires", "expires": "expires",
"path": "Path", "path": "Path",
@ -128,9 +125,8 @@ class Cookie(dict):
class MultiHeader: class MultiHeader:
""" """String-holding object which allow us to set a header within response
Allows us to set a header within response that has a unique key, that has a unique key, but may contain duplicate header names
but may contain duplicate header names
""" """
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name

View File

@ -35,8 +35,8 @@ class ErrorHandler:
self.handlers[exception] = handler self.handlers[exception] = handler
def response(self, request, exception): def response(self, request, exception):
""" """Fetches and executes an exception handler and returns a response
Fetches and executes an exception handler and returns a response object object
:param request: Request :param request: Request
:param exception: Exception to handle :param exception: Exception to handle
@ -86,9 +86,7 @@ class ErrorHandler:
class ContentRangeHandler: class ContentRangeHandler:
""" """Class responsible for parsing request header"""
This class is for parsing the request header
"""
__slots__ = ('start', 'end', 'size', 'total', 'headers') __slots__ = ('start', 'end', 'size', 'total', 'headers')
def __init__(self, request, stats): def __init__(self, request, stats):

View File

@ -16,8 +16,7 @@ DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
class RequestParameters(dict): class RequestParameters(dict):
""" """Hosts a dict with lists as values where get returns the first
Hosts a dict with lists as values where get returns the first
value of the list and getlist returns the whole shebang value of the list and getlist returns the whole shebang
""" """
@ -31,9 +30,7 @@ class RequestParameters(dict):
class Request(dict): 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', 'transport', 'url', 'headers', 'version', 'method', '_cookies', 'transport',
'query_string', 'body', 'query_string', 'body',
@ -73,8 +70,8 @@ class Request(dict):
@property @property
def token(self): def token(self):
""" """Attempt to return the auth header token.
Attempts to return the auth header token.
:return: token related to request :return: token related to request
""" """
auth_header = self.headers.get('Authorization') auth_header = self.headers.get('Authorization')
@ -146,11 +143,10 @@ File = namedtuple('File', ['type', 'body', 'name'])
def parse_multipart_form(body, boundary): def parse_multipart_form(body, boundary):
""" """Parse a request body and returns fields and files
Parses a request body and returns fields and files
:param body: Bytes request body :param body: bytes request body
:param boundary: Bytes multipart boundary :param boundary: bytes multipart boundary
:return: fields (RequestParameters), files (RequestParameters) :return: fields (RequestParameters), files (RequestParameters)
""" """
files = RequestParameters() files = RequestParameters()

View File

@ -1,4 +1,3 @@
from collections import ChainMap
from mimetypes import guess_type from mimetypes import guess_type
from os import path from os import path
from ujson import dumps as json_dumps from ujson import dumps as json_dumps
@ -98,17 +97,15 @@ class HTTPResponse:
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
# This is all returned in a kind-of funky way # This is all returned in a kind-of funky way
# We tried to make this as fast as possible in pure python # We tried to make this as fast as possible in pure python
default_header = dict() timeout_header = b''
if keep_alive: if keep_alive and keep_alive_timeout is not None:
if keep_alive_timeout: timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
default_header['Keep-Alive'] = keep_alive_timeout self.headers['Content-Length'] = self.headers.get(
default_header['Connection'] = 'keep-alive' 'Content-Length', len(self.body))
else: self.headers['Content-Type'] = self.headers.get(
default_header['Connection'] = 'close' 'Content-Type', self.content_type)
default_header['Content-Length'] = len(self.body)
default_header['Content-Type'] = self.content_type
headers = b'' headers = b''
for name, value in ChainMap(self.headers, default_header).items(): for name, value in self.headers.items():
try: try:
headers += ( headers += (
b'%b: %b\r\n' % ( b'%b: %b\r\n' % (
@ -117,6 +114,7 @@ class HTTPResponse:
headers += ( headers += (
b'%b: %b\r\n' % ( b'%b: %b\r\n' % (
str(name).encode(), str(value).encode('utf-8'))) 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
status = COMMON_STATUS_CODES.get(self.status) status = COMMON_STATUS_CODES.get(self.status)
@ -124,11 +122,15 @@ class HTTPResponse:
status = ALL_STATUS_CODES.get(self.status) status = ALL_STATUS_CODES.get(self.status)
return (b'HTTP/%b %d %b\r\n' return (b'HTTP/%b %d %b\r\n'
b'Connection: %b\r\n'
b'%b'
b'%b\r\n' b'%b\r\n'
b'%b') % ( b'%b') % (
version.encode(), version.encode(),
self.status, self.status,
status, status,
b'keep-alive' if keep_alive else b'close',
timeout_header,
headers, headers,
self.body self.body
) )
@ -152,15 +154,32 @@ def json(body, status=200, headers=None, **kwargs):
status=status, content_type="application/json") status=status, content_type="application/json")
def text(body, status=200, headers=None): def text(body, status=200, headers=None,
content_type="text/plain; charset=utf-8"):
""" """
Returns response object with body in text format. Returns response object with body in text format.
:param body: Response data to be encoded. :param body: Response data to be encoded.
:param status: Response code. :param status: Response code.
:param headers: Custom Headers. :param headers: Custom Headers.
:param content_type:
the content type (string) of the response
""" """
return HTTPResponse(body, status=status, headers=headers, return HTTPResponse(body, status=status, headers=headers,
content_type="text/plain; charset=utf-8") content_type=content_type)
def raw(body, status=200, headers=None,
content_type="application/octet-stream"):
"""
Returns response object without encoding the body.
:param body: Response data.
:param status: Response code.
:param headers: Custom Headers.
:param content_type:
the content type (string) of the response
"""
return HTTPResponse(body_bytes=body, status=status, headers=headers,
content_type=content_type)
def html(body, status=200, headers=None): def html(body, status=200, headers=None):
@ -175,8 +194,8 @@ def html(body, status=200, headers=None):
async def file(location, mime_type=None, headers=None, _range=None): async def file(location, mime_type=None, headers=None, _range=None):
""" """Return a response object with file data.
Returns response object with file data.
:param location: Location of file on system. :param location: Location of file on system.
:param mime_type: Specific mime_type. :param mime_type: Specific mime_type.
:param headers: Custom Headers. :param headers: Custom Headers.
@ -203,14 +222,12 @@ async def file(location, mime_type=None, headers=None, _range=None):
def redirect(to, headers=None, status=302, def redirect(to, headers=None, status=302,
content_type="text/html; charset=utf-8"): content_type="text/html; charset=utf-8"):
""" """Abort execution and cause a 302 redirect (by default).
Aborts execution and causes a 302 redirect (by default).
:param to: path or fully qualified URL to redirect to :param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request :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 status: status code (int) of the new request, defaults to 302
:param content_type: :param content_type: the content type (string) of the response
the content type (string) of the response
:returns: the redirecting Response :returns: the redirecting Response
""" """
headers = headers or {} headers = headers or {}

View File

@ -32,8 +32,7 @@ class RouteDoesNotExist(Exception):
class Router: class Router:
""" """Router supports basic routing with parameters and method checks
Router supports basic routing with parameters and method checks
Usage: Usage:
@ -71,8 +70,9 @@ class Router:
self.hosts = None self.hosts = None
def parse_parameter_string(self, parameter_string): def parse_parameter_string(self, parameter_string):
""" """Parse a parameter string into its constituent name, type, and
Parse a parameter string into its constituent name, type, and pattern pattern
For example: For example:
`parse_parameter_string('<param_one:[A-z]')` -> `parse_parameter_string('<param_one:[A-z]')` ->
('param_one', str, '[A-z]') ('param_one', str, '[A-z]')
@ -94,14 +94,13 @@ class Router:
return name, _type, pattern return name, _type, pattern
def add(self, uri, methods, handler, host=None): def add(self, uri, methods, handler, host=None):
""" """Add a handler to the route list
Adds a handler to the route list
:param uri: Path to match :param uri: path to match
:param methods: Array of accepted method names. :param methods: sequence of accepted method names. If none are
If none are provided, any method is allowed provided, any method is allowed
:param handler: Request handler function. :param handler: request handler function.
When executed, it should provide a response object. When executed, it should provide a response object.
:return: Nothing :return: Nothing
""" """
@ -239,8 +238,7 @@ class Router:
@lru_cache(maxsize=ROUTER_CACHE_SIZE) @lru_cache(maxsize=ROUTER_CACHE_SIZE)
def find_route_by_view_name(self, view_name): def find_route_by_view_name(self, view_name):
""" """Find a route in the router based on the specified view name.
Find a route in the router based on the specified view name.
:param view_name: string of view name to search by :param view_name: string of view name to search by
:return: tuple containing (uri, Route) :return: tuple containing (uri, Route)
@ -255,8 +253,7 @@ class Router:
return (None, None) return (None, None)
def get(self, request): def get(self, request):
""" """Get 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 error
:param request: Request object :param request: Request object
@ -270,11 +267,11 @@ class Router:
@lru_cache(maxsize=ROUTER_CACHE_SIZE) @lru_cache(maxsize=ROUTER_CACHE_SIZE)
def _get(self, url, method, host): def _get(self, url, method, host):
""" """Get a request handler based on the URL of the request, or raises an
Gets a request handler based on the URL of the request, or raises an
error. Internal method for caching. error. Internal method for caching.
:param url: Request URL
:param method: Request method :param url: request URL
:param method: request method
:return: handler, arguments, keyword arguments :return: handler, arguments, keyword arguments
""" """
url = host + url url = host + url

View File

@ -2,7 +2,7 @@ import logging
import re import re
import warnings import warnings
from asyncio import get_event_loop from asyncio import get_event_loop
from collections import deque from collections import deque, defaultdict
from functools import partial from functools import partial
from inspect import isawaitable, stack, getmodulename from inspect import isawaitable, stack, getmodulename
from traceback import format_exc from traceback import format_exc
@ -10,18 +10,19 @@ from urllib.parse import urlencode, urlunparse
from .config import Config from .config import Config
from .constants import HTTP_METHODS from .constants import HTTP_METHODS
from .handlers import ErrorHandler
from .exceptions import ServerError, URLBuildError from .exceptions import ServerError, URLBuildError
from .handlers import ErrorHandler
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, serve_multiple, HttpProtocol from .server import serve, serve_multiple, HttpProtocol
from .static import register as static_register from .static import register as static_register
from .views import CompositionView
class Sanic: class Sanic:
def __init__(self, name=None, router=None,
error_handler=None): def __init__(self, name=None, router=None, error_handler=None):
# Only set up a default log handler if the # Only set up a default log handler if the
# end-user application didn't set anything up. # end-user application didn't set anything up.
if not logging.root.handlers and log.level == logging.NOTSET: if not logging.root.handlers and log.level == logging.NOTSET:
@ -31,9 +32,12 @@ class Sanic:
handler.setFormatter(formatter) handler.setFormatter(formatter)
log.addHandler(handler) log.addHandler(handler)
log.setLevel(logging.INFO) log.setLevel(logging.INFO)
# Get name from previous stack frame
if name is None: if name is None:
frame_records = stack()[1] frame_records = stack()[1]
name = getmodulename(frame_records[1]) name = getmodulename(frame_records[1])
self.name = name self.name = name
self.router = router or Router() self.router = router or Router()
self.error_handler = error_handler or ErrorHandler() self.error_handler = error_handler or ErrorHandler()
@ -44,19 +48,49 @@ class Sanic:
self._blueprint_order = [] self._blueprint_order = []
self.debug = None self.debug = None
self.sock = None self.sock = None
self.processes = None self.listeners = defaultdict(list)
# Register alternative method names # Register alternative method names
self.go_fast = self.run self.go_fast = self.run
@property
def loop(self):
"""Synonymous with asyncio.get_event_loop()."""
return get_event_loop()
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# Registration # Registration
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
def add_task(self, task):
"""Schedule a task to run later, after the loop has started.
Different from asyncio.ensure_future in that it does not
also return a future, and the actual ensure_future call
is delayed until before server start.
:param task: future, couroutine or awaitable
"""
@self.listener('before_server_start')
def run(app, loop):
if callable(task):
loop.create_task(task())
else:
loop.create_task(task)
# Decorator
def listener(self, event):
"""Create a listener from a decorated function.
:param event: event to listen to
"""
def decorator(listener):
self.listeners[event].append(listener)
return listener
return decorator
# Decorator # Decorator
def route(self, uri, methods=frozenset({'GET'}), host=None): def route(self, uri, methods=frozenset({'GET'}), host=None):
""" """Decorate 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
:param methods: list or tuple of methods allowed :param methods: list or tuple of methods allowed
@ -99,8 +133,7 @@ class Sanic:
return self.route(uri, methods=frozenset({"DELETE"}), host=host) return self.route(uri, methods=frozenset({"DELETE"}), host=host)
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None): def add_route(self, handler, uri, methods=frozenset({'GET'}), 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
routes. routes.
@ -113,7 +146,16 @@ class Sanic:
""" """
# Handle HTTPMethodView differently # Handle HTTPMethodView differently
if hasattr(handler, 'view_class'): if hasattr(handler, 'view_class'):
methods = frozenset(HTTP_METHODS) methods = set()
for method in HTTP_METHODS:
if getattr(handler.view_class, method.lower(), None):
methods.add(method)
# handle composition view differently
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
self.route(uri=uri, methods=methods, host=host)(handler) self.route(uri=uri, methods=methods, host=host)(handler)
return handler return handler
@ -122,8 +164,7 @@ class Sanic:
# Decorator # Decorator
def exception(self, *exceptions): def exception(self, *exceptions):
""" """Decorate a function to be registered as a handler for exceptions
Decorates a function to be registered as a handler for exceptions
:param exceptions: exceptions :param exceptions: exceptions
:return: decorated function :return: decorated function
@ -137,14 +178,11 @@ class Sanic:
return response return response
# Decorator # Decorator
def middleware(self, *args, **kwargs): def middleware(self, middleware_or_request):
"""Decorate and register middleware to be called before a request.
Can either be called as @app.middleware or @app.middleware('request')
""" """
Decorates and registers middleware to be called before a request def register_middleware(middleware, attach_to='request'):
can either be called as @app.middleware or @app.middleware('request')
"""
attach_to = 'request'
def register_middleware(middleware):
if attach_to == 'request': if attach_to == 'request':
self.request_middleware.append(middleware) self.request_middleware.append(middleware)
if attach_to == 'response': if attach_to == 'response':
@ -152,25 +190,24 @@ class Sanic:
return middleware return middleware
# Detect which way this was called, @middleware or @middleware('AT') # Detect which way this was called, @middleware or @middleware('AT')
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): if callable(middleware_or_request):
return register_middleware(args[0]) return register_middleware(middleware_or_request)
else: else:
attach_to = args[0] return partial(register_middleware,
return register_middleware attach_to=middleware_or_request)
# Static Files # Static Files
def static(self, uri, file_or_directory, pattern='.+', def static(self, uri, file_or_directory, pattern='.+',
use_modified_since=True, use_content_range=False): use_modified_since=True, use_content_range=False):
""" """Register a root to serve files from. The input can either be a
Registers a root to serve files from. The input can either be a file file or a directory. See
or a directory. See
""" """
static_register(self, uri, file_or_directory, pattern, static_register(self, uri, file_or_directory, pattern,
use_modified_since, use_content_range) use_modified_since, use_content_range)
def blueprint(self, blueprint, **options): def blueprint(self, blueprint, **options):
""" """Register a blueprint on the application.
Registers a blueprint on the application.
:param blueprint: Blueprint object :param blueprint: Blueprint object
:param options: option dictionary with blueprint defaults :param options: option dictionary with blueprint defaults
@ -197,7 +234,7 @@ class Sanic:
return self.blueprint(*args, **kwargs) return self.blueprint(*args, **kwargs)
def url_for(self, view_name: str, **kwargs): def url_for(self, view_name: str, **kwargs):
"""Builds a URL based on a view name and the values provided. """Build a URL based on a view name and the values provided.
In order to build a URL, all request parameters must be supplied as In order to build a URL, all request parameters must be supplied as
keyword arguments, and each parameter must pass the test for the keyword arguments, and each parameter must pass the test for the
@ -207,7 +244,7 @@ class Sanic:
Keyword arguments that are not request parameters will be included in Keyword arguments that are not request parameters will be included in
the output URL's query string. the output URL's query string.
:param view_name: A string referencing the view name :param view_name: string referencing the view name
:param **kwargs: keys and values that are used to build request :param **kwargs: keys and values that are used to build request
parameters and query string arguments. parameters and query string arguments.
@ -230,6 +267,19 @@ class Sanic:
matched_params = re.findall( matched_params = re.findall(
self.router.parameter_pattern, uri) self.router.parameter_pattern, uri)
# _method is only a placeholder now, don't know how to support it
kwargs.pop('_method', None)
anchor = kwargs.pop('_anchor', '')
# _external need SERVER_NAME in config or pass _server arg
external = kwargs.pop('_external', False)
scheme = kwargs.pop('_scheme', '')
if scheme and not external:
raise ValueError('When specifying _scheme, _external must be True')
netloc = kwargs.pop('_server', None)
if netloc is None and external:
netloc = self.config.get('SERVER_NAME', '')
for match in matched_params: for match in matched_params:
name, _type, pattern = self.router.parse_parameter_string( name, _type, pattern = self.router.parse_parameter_string(
match) match)
@ -270,12 +320,9 @@ class Sanic:
replacement_regex, supplied_param, out) replacement_regex, supplied_param, out)
# parse the remainder of the keyword arguments into a querystring # parse the remainder of the keyword arguments into a querystring
if kwargs: query_string = urlencode(kwargs, doseq=True) if kwargs else ''
query_string = urlencode(kwargs) # scheme://netloc/path;parameters?query#fragment
out = urlunparse(( out = urlunparse((scheme, netloc, out, '', query_string, anchor))
'', '', out,
'', query_string, ''
))
return out return out
@ -287,9 +334,8 @@ class Sanic:
pass pass
async def handle_request(self, request, response_callback): async def handle_request(self, request, response_callback):
""" """Take a request from the HTTP Server and return a response object
Takes a request from the HTTP Server and returns a response object to to be sent back The HTTP Server only expects a response object, so
be sent back The HTTP Server only expects a response object, so
exception handling must be done here exception handling must be done here
:param request: HTTP Request object :param request: HTTP Request object
@ -371,9 +417,8 @@ class Sanic:
after_start=None, before_stop=None, after_stop=None, ssl=None, after_start=None, before_stop=None, after_stop=None, ssl=None,
sock=None, workers=1, loop=None, protocol=HttpProtocol, sock=None, workers=1, loop=None, protocol=HttpProtocol,
backlog=100, stop_event=None, register_sys_signals=True): backlog=100, stop_event=None, register_sys_signals=True):
""" """Run the HTTP Server and listen until keyboard interrupt or term
Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drain 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
@ -403,6 +448,7 @@ class Sanic:
after_stop=after_stop, ssl=ssl, sock=sock, workers=workers, after_stop=after_stop, ssl=ssl, sock=sock, workers=workers,
loop=loop, protocol=protocol, backlog=backlog, loop=loop, protocol=protocol, backlog=backlog,
stop_event=stop_event, register_sys_signals=register_sys_signals) stop_event=stop_event, register_sys_signals=register_sys_signals)
try: try:
if workers == 1: if workers == 1:
serve(**server_settings) serve(**server_settings)
@ -422,9 +468,7 @@ class Sanic:
before_stop=None, after_stop=None, ssl=None, before_stop=None, after_stop=None, ssl=None,
sock=None, loop=None, protocol=HttpProtocol, sock=None, loop=None, protocol=HttpProtocol,
backlog=100, stop_event=None): backlog=100, stop_event=None):
""" """Asynchronous version of `run`."""
Asynchronous version of `run`.
"""
server_settings = self._helper( server_settings = self._helper(
host=host, port=port, debug=debug, before_start=before_start, host=host, port=port, debug=debug, before_start=before_start,
after_start=after_start, before_stop=before_stop, after_start=after_start, before_stop=before_stop,
@ -445,9 +489,7 @@ class Sanic:
after_stop=None, ssl=None, sock=None, workers=1, loop=None, after_stop=None, ssl=None, sock=None, workers=1, loop=None,
protocol=HttpProtocol, backlog=100, stop_event=None, protocol=HttpProtocol, backlog=100, stop_event=None,
register_sys_signals=True, run_async=False): register_sys_signals=True, run_async=False):
""" """Helper function used by `run` and `create_server`."""
Helper function used by `run` and `create_server`.
"""
if loop is not None: if loop is not None:
if debug: if debug:
@ -457,9 +499,19 @@ class Sanic:
"pull/335 has more information.", "pull/335 has more information.",
DeprecationWarning) DeprecationWarning)
# Deprecate this
if any(arg is not None for arg in (after_stop, after_start,
before_start, before_stop)):
if debug:
warnings.simplefilter('default')
warnings.warn("Passing a before_start, before_stop, after_start or"
"after_stop callback will be deprecated in next "
"major version after 0.4.0",
DeprecationWarning)
self.error_handler.debug = debug self.error_handler.debug = debug
self.debug = debug self.debug = debug
self.loop = loop = get_event_loop() loop = self.loop
server_settings = { server_settings = {
'protocol': protocol, 'protocol': protocol,
@ -481,19 +533,18 @@ class Sanic:
# Register start/stop events # Register start/stop events
# -------------------------------------------- # # -------------------------------------------- #
for event_name, settings_name, args, reverse in ( for event_name, settings_name, reverse, args in (
("before_server_start", "before_start", before_start, False), ("before_server_start", "before_start", False, before_start),
("after_server_start", "after_start", after_start, False), ("after_server_start", "after_start", False, after_start),
("before_server_stop", "before_stop", before_stop, True), ("before_server_stop", "before_stop", True, before_stop),
("after_server_stop", "after_stop", after_stop, True), ("after_server_stop", "after_stop", True, after_stop),
): ):
listeners = [] listeners = self.listeners[event_name].copy()
for blueprint in self.blueprints.values():
listeners += blueprint.listeners[event_name]
if args: if args:
if callable(args): if callable(args):
args = [args] listeners.append(args)
listeners += args else:
listeners.extend(args)
if reverse: if reverse:
listeners.reverse() listeners.reverse()
# Prepend sanic to the arguments when listeners are triggered # Prepend sanic to the arguments when listeners are triggered

View File

@ -33,8 +33,7 @@ class Signal:
class CIDict(dict): class CIDict(dict):
""" """Case Insensitive dict where all keys are converted to lowercase
Case Insensitive dict where all keys are converted to lowercase
This does not maintain the inputted case when calling items() or keys() This does not maintain the inputted case when calling items() or keys()
in favor of speed, since headers are case insensitive in favor of speed, since headers are case insensitive
""" """
@ -228,8 +227,8 @@ class HttpProtocol(asyncio.Protocol):
self._total_request_size = 0 self._total_request_size = 0
def close_if_idle(self): def close_if_idle(self):
""" """Close the connection if a request is not being sent or received
Close the connection if a request is not being sent or received
:return: boolean - True if closed, false if staying open :return: boolean - True if closed, false if staying open
""" """
if not self.parser: if not self.parser:
@ -239,9 +238,8 @@ class HttpProtocol(asyncio.Protocol):
def update_current_time(loop): def update_current_time(loop):
""" """Cache the current time, since it is needed at the end of every
Caches the current time, since it is needed keep-alive request to update the request timeout time
at the end of every keep-alive request to update the request timeout time
:param loop: :param loop:
:return: :return:
@ -252,17 +250,15 @@ def update_current_time(loop):
def trigger_events(events, loop): def trigger_events(events, loop):
""" """Trigger event callbacks (functions or async)
:param events: one or more sync or async functions to execute :param events: one or more sync or async functions to execute
:param loop: event loop :param loop: event loop
""" """
if events: for event in events:
if not isinstance(events, list): result = event(loop)
events = [events] if isawaitable(result):
for event in events: loop.run_until_complete(result)
result = event(loop)
if isawaitable(result):
loop.run_until_complete(result)
def serve(host, port, request_handler, error_handler, before_start=None, def serve(host, port, request_handler, error_handler, before_start=None,
@ -270,31 +266,30 @@ def serve(host, port, request_handler, error_handler, before_start=None,
request_timeout=60, ssl=None, sock=None, request_max_size=None, request_timeout=60, ssl=None, sock=None, request_max_size=None,
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
register_sys_signals=True, run_async=False): register_sys_signals=True, run_async=False):
""" """Start 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 error_handler: Sanic error handler with middleware
:param before_start: Function to be executed before the server starts :param before_start: function to be executed before the server starts
listening. Takes arguments `app` instance and `loop` listening. Takes arguments `app` instance and `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 arguments `app` instance and `loop` listening. Takes arguments `app` instance and `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 arguments received before it is respected. Takes arguments
`app` instance and `loop` `app` instance and `loop`
:param after_stop: Function to be executed when a stop signal is :param after_stop: function to be executed when a stop signal is
received after it is respected. Takes arguments received after it is respected. Takes arguments
`app` instance and `loop` `app` instance and `loop`
:param debug: Enables debug output (slows server) :param debug: enables debug output (slows server)
:param request_timeout: time in seconds :param request_timeout: time in seconds
:param ssl: SSLContext :param ssl: SSLContext
:param sock: Socket for the server to accept connections from :param sock: Socket for the server to accept connections from
:param request_max_size: size in bytes, `None` for no limit :param request_max_size: size in bytes, `None` for no limit
: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 :param protocol: subclass of asyncio protocol class
:return: Nothing :return: Nothing
""" """
if not run_async: if not run_async:
@ -349,9 +344,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
try: try:
loop.add_signal_handler(_signal, loop.stop) loop.add_signal_handler(_signal, loop.stop)
except NotImplementedError: except NotImplementedError:
log.warn(('Sanic tried to use loop.add_signal_handler') log.warn('Sanic tried to use loop.add_signal_handler but it is'
('but it is not implemented on this platform.')) ' not implemented on this platform.')
pid = os.getpid() pid = os.getpid()
try: try:
log.info('Starting worker [{}]'.format(pid)) log.info('Starting worker [{}]'.format(pid))
@ -380,9 +374,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
def serve_multiple(server_settings, workers, stop_event=None): def serve_multiple(server_settings, workers, stop_event=None):
""" """Start multiple server processes simultaneously. Stop on interrupt
Starts multiple server processes simultaneously. Stops on interrupt and terminate signals, and drain connections when complete.
and terminate signals, and drains connections when complete.
:param server_settings: kw arguments to be passed to the serve function :param server_settings: kw arguments to be passed to the serve function
:param workers: number of workers to launch :param workers: number of workers to launch

View File

@ -18,7 +18,7 @@ def register(app, uri, file_or_directory, pattern,
# make a good effort here. Modified-since is nice, but we could # make a good effort here. Modified-since is nice, but we could
# also look into etags, expires, and caching # also look into etags, expires, and caching
""" """
Registers a static directory handler with Sanic by adding a route to the Register a static directory handler with Sanic by adding a route to the
router and registering a handler. router and registering a handler.
:param app: Sanic :param app: Sanic

View File

@ -6,7 +6,11 @@ PORT = 42101
async def local_request(method, uri, cookies=None, *args, **kwargs): async def local_request(method, uri, cookies=None, *args, **kwargs):
url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri) if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
url = uri
else:
url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri)
log.info(url) log.info(url)
async with aiohttp.ClientSession(cookies=cookies) as session: async with aiohttp.ClientSession(cookies=cookies) as session:
async with getattr( async with getattr(
@ -17,8 +21,8 @@ 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,
debug=False, server_kwargs={}, debug=False, server_kwargs={}, *request_args,
*request_args, **request_kwargs): **request_kwargs):
results = [None, None] results = [None, None]
exceptions = [] exceptions = []
@ -28,6 +32,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
results[0] = request results[0] = request
app.request_middleware.appendleft(_collect_request) app.request_middleware.appendleft(_collect_request)
@app.listener('after_server_start')
async def _collect_response(sanic, loop): async def _collect_response(sanic, loop):
try: try:
response = await local_request(method, uri, *request_args, response = await local_request(method, uri, *request_args,
@ -37,8 +42,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=PORT, app.run(host=HOST, debug=debug, port=PORT, **server_kwargs)
after_start=_collect_response, **server_kwargs) app.listeners['after_server_start'].pop()
if exceptions: if exceptions:
raise ValueError("Exception during request: {}".format(exceptions)) raise ValueError("Exception during request: {}".format(exceptions))

View File

@ -1,8 +1,9 @@
from .exceptions import InvalidUsage from .exceptions import InvalidUsage
from .constants import HTTP_METHODS
class HTTPMethodView: class HTTPMethodView:
""" Simple class based implementation of view for the sanic. """Simple class based implementation of view for the sanic.
You should implement methods (get, post, put, patch, delete) for the class You should implement methods (get, post, put, patch, delete) for the class
to every HTTP method you want to support. to every HTTP method you want to support.
@ -40,17 +41,12 @@ class HTTPMethodView:
def dispatch_request(self, request, *args, **kwargs): def dispatch_request(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None) handler = getattr(self, request.method.lower(), None)
if handler: return handler(request, *args, **kwargs)
return handler(request, *args, **kwargs)
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405)
@classmethod @classmethod
def as_view(cls, *class_args, **class_kwargs): def as_view(cls, *class_args, **class_kwargs):
""" Converts the class into an actual view function that can be used """Return view function for use with the routing system, that
with the routing system. dispatches request to appropriate handler method.
""" """
def view(*args, **kwargs): def view(*args, **kwargs):
self = view.view_class(*class_args, **class_kwargs) self = view.view_class(*class_args, **class_kwargs)
@ -69,7 +65,7 @@ class HTTPMethodView:
class CompositionView: class CompositionView:
""" Simple method-function mapped view for the sanic. """Simple method-function mapped view for the sanic.
You can add handler functions to methods (get, post, put, patch, delete) You can add handler functions to methods (get, post, put, patch, delete)
for every HTTP method you want to support. for every HTTP method you want to support.
@ -89,15 +85,15 @@ class CompositionView:
def add(self, methods, handler): def add(self, methods, handler):
for method in methods: for method in methods:
if method not in HTTP_METHODS:
raise InvalidUsage(
'{} is not a valid HTTP method.'.format(method))
if method in self.handlers: if method in self.handlers:
raise KeyError( raise InvalidUsage(
'Method {} already is registered.'.format(method)) 'Method {} is already registered.'.format(method))
self.handlers[method] = handler self.handlers[method] = handler
def __call__(self, request, *args, **kwargs): def __call__(self, request, *args, **kwargs):
handler = self.handlers.get(request.method.upper(), None) handler = self.handlers[request.method.upper()]
if handler is None:
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405)
return handler(request, *args, **kwargs) return handler(request, *args, **kwargs)

View File

@ -5,6 +5,7 @@ from sanic import Sanic
def test_bad_request_response(): def test_bad_request_response():
app = Sanic('test_bad_request_response') app = Sanic('test_bad_request_response')
lines = [] lines = []
@app.listener('after_server_start')
async def _request(sanic, loop): async def _request(sanic, loop):
connect = asyncio.open_connection('127.0.0.1', 42101) connect = asyncio.open_connection('127.0.0.1', 42101)
reader, writer = await connect reader, writer = await connect
@ -15,6 +16,6 @@ def test_bad_request_response():
break break
lines.append(line) lines.append(line)
app.stop() app.stop()
app.run(host='127.0.0.1', port=42101, debug=False, after_start=_request) app.run(host='127.0.0.1', port=42101, debug=False)
assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n' assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n'
assert lines[-1] == b'Error: Bad Request' assert lines[-1] == b'Error: Bad Request'

30
tests/test_create_task.py Normal file
View File

@ -0,0 +1,30 @@
import sanic
from sanic.utils import sanic_endpoint_test
from sanic.response import text
from threading import Event
import asyncio
def test_create_task():
e = Event()
async def coro():
await asyncio.sleep(0.05)
e.set()
app = sanic.Sanic()
app.add_task(coro)
@app.route('/early')
def not_set(request):
return text(e.is_set())
@app.route('/late')
async def set(request):
await asyncio.sleep(0.1)
return text(e.is_set())
request, response = sanic_endpoint_test(app, uri='/early')
assert response.body == b'False'
request, response = sanic_endpoint_test(app, uri='/late')
assert response.body == b'True'

View File

@ -9,10 +9,10 @@ from sanic import Sanic
from sanic.utils import HOST, PORT from sanic.utils import HOST, PORT
AVAILABLE_LISTENERS = [ AVAILABLE_LISTENERS = [
'before_start', 'before_server_start',
'after_start', 'after_server_start',
'before_stop', 'before_server_stop',
'after_stop' 'after_server_stop'
] ]
@ -42,9 +42,10 @@ def test_single_listener(listener_name):
random_name_app = Sanic(''.join( random_name_app = Sanic(''.join(
[choice(ascii_letters) for _ in range(choice(range(5, 10)))])) [choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
output = list() output = list()
start_stop_app( # Register listener
random_name_app, random_name_app.listener(listener_name)(
**{listener_name: create_listener(listener_name, output)}) create_listener(listener_name, output))
start_stop_app(random_name_app)
assert random_name_app.name + listener_name == output.pop() assert random_name_app.name + listener_name == output.pop()
@ -52,9 +53,9 @@ def test_all_listeners():
random_name_app = Sanic(''.join( random_name_app = Sanic(''.join(
[choice(ascii_letters) for _ in range(choice(range(5, 10)))])) [choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
output = list() output = list()
start_stop_app( for listener_name in AVAILABLE_LISTENERS:
random_name_app, listener = create_listener(listener_name, output)
**{listener_name: create_listener(listener_name, output) random_name_app.listener(listener_name)(listener)
for listener_name in AVAILABLE_LISTENERS}) start_stop_app(random_name_app)
for listener_name in AVAILABLE_LISTENERS: for listener_name in AVAILABLE_LISTENERS:
assert random_name_app.name + listener_name == output.pop() assert random_name_app.name + listener_name == output.pop()

View File

@ -27,10 +27,11 @@ def test_register_system_signals():
async def hello_route(request): async def hello_route(request):
return HTTPResponse() return HTTPResponse()
app.run(HOST, PORT, app.listener('after_server_start')(stop)
before_start=set_loop, app.listener('before_server_start')(set_loop)
after_start=stop, app.listener('after_server_stop')(after)
after_stop=after)
app.run(HOST, PORT)
assert calledq.get() == True assert calledq.get() == True
@ -42,9 +43,9 @@ def test_dont_register_system_signals():
async def hello_route(request): async def hello_route(request):
return HTTPResponse() return HTTPResponse()
app.run(HOST, PORT, app.listener('after_server_start')(stop)
before_start=set_loop, app.listener('before_server_start')(set_loop)
after_start=stop, app.listener('after_server_stop')(after)
after_stop=after,
register_sys_signals=False) app.run(HOST, PORT, register_sys_signals=False)
assert calledq.get() == False assert calledq.get() == False

View File

@ -5,11 +5,19 @@ from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.views import HTTPMethodView from sanic.views import HTTPMethodView
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.utils import sanic_endpoint_test from sanic.utils import sanic_endpoint_test, PORT as test_port
from sanic.exceptions import URLBuildError from sanic.exceptions import URLBuildError
import string import string
URL_FOR_ARGS1 = dict(arg1=['v1', 'v2'])
URL_FOR_VALUE1 = '/myurl?arg1=v1&arg1=v2'
URL_FOR_ARGS2 = dict(arg1=['v1', 'v2'], _anchor='anchor')
URL_FOR_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor'
URL_FOR_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http',
_server='localhost:{}'.format(test_port), _external=True)
URL_FOR_VALUE3 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(test_port)
def _generate_handlers_from_names(app, l): def _generate_handlers_from_names(app, l):
for name in l: for name in l:
@ -39,6 +47,23 @@ def test_simple_url_for_getting(simple_app):
assert response.text == letter assert response.text == letter
@pytest.mark.parametrize('args,url',
[(URL_FOR_ARGS1, URL_FOR_VALUE1),
(URL_FOR_ARGS2, URL_FOR_VALUE2),
(URL_FOR_ARGS3, URL_FOR_VALUE3)])
def test_simple_url_for_getting_with_more_params(args, url):
app = Sanic('more_url_build')
@app.route('/myurl')
def passes(request):
return text('this should pass')
assert url == app.url_for('passes', **args)
request, response = sanic_endpoint_test(app, uri=url)
assert response.status == 200
assert response.text == 'this should pass'
def test_fails_if_endpoint_not_found(): def test_fails_if_endpoint_not_found():
app = Sanic('fail_url_build') app = Sanic('fail_url_build')
@ -75,6 +100,19 @@ def test_fails_url_build_if_param_not_passed():
assert 'Required parameter `Z` was not passed to url_for' in str(e.value) assert 'Required parameter `Z` was not passed to url_for' in str(e.value)
def test_fails_url_build_if_params_not_passed():
app = Sanic('fail_url_build')
@app.route('/fail')
def fail():
return text('this should fail')
with pytest.raises(ValueError) as e:
app.url_for('fail', _scheme='http')
assert str(e.value) == 'When specifying _scheme, _external must be True'
COMPLEX_PARAM_URL = ( COMPLEX_PARAM_URL = (
'/<foo:int>/<four_letter_string:[A-z]{4}>/' '/<foo:int>/<four_letter_string:[A-z]{4}>/'
'<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:number>') '<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:number>')
@ -179,11 +217,11 @@ def blueprint_app():
return text( return text(
'foo from first : {}'.format(param)) 'foo from first : {}'.format(param))
@second_print.route('/foo') # noqa @second_print.route('/foo') # noqa
def foo(): def foo():
return text('foo from second') return text('foo from second')
@second_print.route('/foo/<param>') # noqa @second_print.route('/foo/<param>') # noqa
def foo_with_param(request, param): def foo_with_param(request, param):
return text( return text(
'foo from second : {}'.format(param)) 'foo from second : {}'.format(param))

View File

@ -1,8 +1,9 @@
import pytest as pytest import pytest as pytest
from sanic import Sanic from sanic import Sanic
from sanic.exceptions import InvalidUsage
from sanic.response import text, HTTPResponse from sanic.response import text, HTTPResponse
from sanic.views import HTTPMethodView from sanic.views import HTTPMethodView, CompositionView
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.request import Request from sanic.request import Request
from sanic.utils import sanic_endpoint_test from sanic.utils import sanic_endpoint_test
@ -196,3 +197,65 @@ def test_with_decorator():
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'
assert results[0] == 1 assert results[0] == 1
def test_composition_view_rejects_incorrect_methods():
def foo(request):
return text('Foo')
view = CompositionView()
with pytest.raises(InvalidUsage) as e:
view.add(['GET', 'FOO'], foo)
assert str(e.value) == 'FOO is not a valid HTTP method.'
def test_composition_view_rejects_duplicate_methods():
def foo(request):
return text('Foo')
view = CompositionView()
with pytest.raises(InvalidUsage) as e:
view.add(['GET', 'POST', 'GET'], foo)
assert str(e.value) == 'Method GET is already registered.'
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_composition_view_runs_methods_as_expected(method):
app = Sanic('test_composition_view')
view = CompositionView()
view.add(['GET', 'POST', 'PUT'], lambda x: text('first method'))
view.add(['DELETE', 'PATCH'], lambda x: text('second method'))
app.add_route(view, '/')
if method in ['GET', 'POST', 'PUT']:
request, response = sanic_endpoint_test(app, uri='/', method=method)
assert response.text == 'first method'
if method in ['DELETE', 'PATCH']:
request, response = sanic_endpoint_test(app, uri='/', method=method)
assert response.text == 'second method'
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_composition_view_rejects_invalid_methods(method):
app = Sanic('test_composition_view')
view = CompositionView()
view.add(['GET', 'POST', 'PUT'], lambda x: text('first method'))
app.add_route(view, '/')
if method in ['GET', 'POST', 'PUT']:
request, response = sanic_endpoint_test(app, uri='/', method=method)
assert response.status == 200
assert response.text == 'first method'
if method in ['DELETE', 'PATCH']:
request, response = sanic_endpoint_test(app, uri='/', method=method)
assert response.status == 405