Merge pull request #2 from channelcat/master

merge
This commit is contained in:
Marcin Baran 2017-01-09 16:59:01 +01:00 committed by GitHub
commit d93da2c1a6
54 changed files with 2026 additions and 219 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,14 +1,10 @@
sudo: false
language: python language: python
python: python:
- '3.5' - '3.5'
install: - '3.6'
- pip install -r requirements.txt install: pip install tox-travis
- pip install -r requirements-dev.txt script: tox
- 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

@ -1,5 +1,7 @@
# Sanic # Sanic
[![Join the chat at https://gitter.im/sanic-python/Lobby](https://badges.gitter.im/sanic-python/Lobby.svg)](https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Build Status](https://travis-ci.org/channelcat/sanic.svg?branch=master)](https://travis-ci.org/channelcat/sanic) [![Build Status](https://travis-ci.org/channelcat/sanic.svg?branch=master)](https://travis-ci.org/channelcat/sanic)
[![PyPI](https://img.shields.io/pypi/v/sanic.svg)](https://pypi.python.org/pypi/sanic/) [![PyPI](https://img.shields.io/pypi/v/sanic.svg)](https://pypi.python.org/pypi/sanic/)
[![PyPI](https://img.shields.io/pypi/pyversions/sanic.svg)](https://pypi.python.org/pypi/sanic/) [![PyPI](https://img.shields.io/pypi/pyversions/sanic.svg)](https://pypi.python.org/pypi/sanic/)
@ -31,13 +33,17 @@ All tests were run on an AWS medium instance running ubuntu, using 1 process. E
from sanic import Sanic from sanic import Sanic
from sanic.response import json from sanic.response import json
app = Sanic(__name__)
app = Sanic()
@app.route("/") @app.route("/")
async def test(request): async def test(request):
return json({ "hello": "world" }) return json({"hello": "world"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
app.run(host="0.0.0.0", port=8000)
``` ```
## Installation ## Installation
@ -50,8 +56,11 @@ app.run(host="0.0.0.0", port=8000)
* [Middleware](docs/middleware.md) * [Middleware](docs/middleware.md)
* [Exceptions](docs/exceptions.md) * [Exceptions](docs/exceptions.md)
* [Blueprints](docs/blueprints.md) * [Blueprints](docs/blueprints.md)
* [Class Based Views](docs/class_based_views.md)
* [Cookies](docs/cookies.md) * [Cookies](docs/cookies.md)
* [Static Files](docs/static_files.md) * [Static Files](docs/static_files.md)
* [Custom Protocol](docs/custom_protocol.md)
* [Testing](docs/testing.md)
* [Deploying](docs/deploying.md) * [Deploying](docs/deploying.md)
* [Contributing](docs/contributing.md) * [Contributing](docs/contributing.md)
* [License](LICENSE) * [License](LICENSE)

58
docs/class_based_views.md Normal file
View File

@ -0,0 +1,58 @@
# Class based views
Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone tries to use a method that has not been implemented, there will be 405 response.
## Examples
```python
from sanic import Sanic
from sanic.views import HTTPMethodView
from sanic.response import text
app = Sanic('some_name')
class SimpleView(HTTPMethodView):
def get(self, request):
return text('I am get method')
def post(self, request):
return text('I am post method')
def put(self, request):
return text('I am put method')
def patch(self, request):
return text('I am patch method')
def delete(self, request):
return text('I am delete method')
app.add_route(SimpleView.as_view(), '/')
```
If you need any url params just mention them in method definition:
```python
class NameView(HTTPMethodView):
def get(self, request, name):
return text('Hello {}'.format(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

@ -27,3 +27,23 @@ async def handler(request):
app.run(host="0.0.0.0", port=8000) app.run(host="0.0.0.0", port=8000)
``` ```
## Middleware chain
If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that you do **not return** any response in your middleware:
```python
app = Sanic(__name__)
@app.middleware('response')
async def custom_banner(request, response):
response.headers["Server"] = "Fake-Server"
@app.middleware('response')
async def prevent_xss(request, response):
response.headers["x-xss-protection"] = "1; mode=block"
app.run(host="0.0.0.0", port=8000)
```
The above code will apply the two middlewares in order. First the middleware **custom_banner** will change the HTTP Response headers *Server* by *Fake-Server*, and the second middleware **prevent_xss** will add the HTTP Headers for prevent Cross-Site-Scripting (XSS) attacks.

View File

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

51
docs/testing.md Normal file
View File

@ -0,0 +1,51 @@
# Testing
Sanic endpoints can be tested locally using the `sanic.utils` module, which
depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
library. The `sanic_endpoint_test` function runs a local server, issues a
configurable request to an endpoint, and returns the result. It takes the
following arguments:
- `app` An instance of a Sanic app.
- `method` *(default `'get'`)* A string representing the HTTP method to use.
- `uri` *(default `'/'`)* A string representing the endpoint to test.
- `gather_request` *(default `True`)* A boolean which determines whether the
original request will be returned by the function. If set to `True`, the
return value is a tuple of `(request, response)`, if `False` only the
response is returned.
- `loop` *(default `None`)* The event loop to use.
- `debug` *(default `False`)* A boolean which determines whether to run the
server in debug mode.
The function further takes the `*request_args` and `**request_kwargs`, which
are passed directly to the aiohttp ClientSession request. For example, to
supply data with a GET request, `method` would be `get` and the keyword
argument `params={'value', 'key'}` would be supplied. More information about
the available arguments to aiohttp can be found
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
Below is a complete example of an endpoint test,
using [pytest](http://doc.pytest.org/en/latest/). The test checks that the
`/challenge` endpoint responds to a GET request with a supplied challenge
string.
```python
import pytest
import aiohttp
from sanic.utils import sanic_endpoint_test
# Import the Sanic app, usually created with Sanic(__name__)
from external_server import app
def test_endpoint_challenge():
# Create the challenge data
request_data = {'challenge': 'dummy_challenge'}
# Send the request to the endpoint, using the default `get` method
request, response = sanic_endpoint_test(app,
uri='/challenge',
params=request_data)
# Assert that the server responds with the challenge string
assert response.text == request_data['challenge']
```

41
examples/cache_example.py Normal file
View File

@ -0,0 +1,41 @@
"""
Example of caching using aiocache package. To run it you will need a Redis
instance running in localhost:6379.
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.
If you want more info about the package check
https://github.com/argaen/aiocache
"""
import asyncio
import aiocache
from sanic import Sanic
from sanic.response import json
from sanic.log import log
from aiocache import cached
from aiocache.serializers import JsonSerializer
app = Sanic(__name__)
aiocache.settings.set_defaults(
cache="aiocache.RedisCache"
)
@cached(key="my_custom_key", serializer=JsonSerializer())
async def expensive_call():
log.info("Expensive has been called")
await asyncio.sleep(3)
return {"test": True}
@app.route("/")
async def test(request):
log.info("Received GET /")
return json(await expensive_call())
app.run(host="0.0.0.0", port=8000, loop=asyncio.get_event_loop())

View File

@ -0,0 +1,60 @@
"""
Example intercepting uncaught exceptions using Sanic's error handler framework.
This may be useful for developers wishing to use Sentry, Airbrake, etc.
or a custom system to log and monitor unexpected errors in production.
First we create our own class inheriting from Handler in sanic.exceptions,
and pass in an instance of it when we create our Sanic instance. Inside this
class' default handler, we can do anything including sending exceptions to
an external service.
"""
"""
Imports and code relevant for our CustomHandler class
(Ordinarily this would be in a separate file)
"""
from sanic.response import text
from sanic.exceptions import Handler, SanicException
class CustomHandler(Handler):
def default(self, request, exception):
# Here, we have access to the exception object
# and can do anything with it (log, send to external service, etc)
# Some exceptions are trivial and built into Sanic (404s, etc)
if not issubclass(type(exception), SanicException):
print(exception)
# Then, we must finish handling the exception by returning
# our response to the client
# For this we can just call the super class' default handler
return super.default(self, request, exception)
"""
This is an ordinary Sanic server, with the exception that we set the
server's error_handler to an instance of our CustomHandler
"""
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
handler = CustomHandler(sanic=app)
app.error_handler = handler
@app.route("/")
async def test(request):
# Here, something occurs which causes an unexpected exception
# This exception will flow to our custom handler.
x = 1 / 0
return json({"test": True})
app.run(host="0.0.0.0", port=8000, debug=True)

18
examples/jinja_example.py Normal file
View File

@ -0,0 +1,18 @@
## To use this example:
# curl -d '{"name": "John Doe"}' localhost:8000
from sanic import Sanic
from sanic.response import html
from jinja2 import Template
template = Template('Hello {{ name }}!')
app = Sanic(__name__)
@app.route('/')
async def test(request):
data = request.json
return html(template.render(**data))
app.run(host="0.0.0.0", port=8000)

View File

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

View File

@ -0,0 +1,21 @@
from sanic import Sanic
import asyncio
from sanic.response import text
from sanic.config import Config
from sanic.exceptions import RequestTimeout
Config.REQUEST_TIMEOUT = 1
app = Sanic(__name__)
@app.route('/')
async def test(request):
await asyncio.sleep(3)
return text('Hello, world!')
@app.exception(RequestTimeout)
def timeout(request, exception):
return text('RequestTimeout from error_handler.', 408)
app.run(host='0.0.0.0', port=8000)

View File

@ -0,0 +1,65 @@
""" To run this example you need additional aiopg package
"""
import os
import asyncio
import uvloop
import aiopg
from sanic import Sanic
from sanic.response import json
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
database_name = os.environ['DATABASE_NAME']
database_host = os.environ['DATABASE_HOST']
database_user = os.environ['DATABASE_USER']
database_password = os.environ['DATABASE_PASSWORD']
connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user,
database_password,
database_host,
database_name)
loop = asyncio.get_event_loop()
async def get_pool():
return await aiopg.create_pool(connection)
app = Sanic(name=__name__)
pool = loop.run_until_complete(get_pool())
async def prepare_db():
""" Let's create some table and add some data
"""
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute('DROP TABLE IF EXISTS sanic_polls')
await cur.execute("""CREATE TABLE sanic_polls (
id serial primary key,
question varchar(50),
pub_date timestamp
);""")
for i in range(0, 100):
await cur.execute("""INSERT INTO sanic_polls
(id, question, pub_date) VALUES ({}, {}, now())
""".format(i, i))
@app.route("/")
async def handle(request):
async with pool.acquire() as conn:
async with conn.cursor() as cur:
result = []
await cur.execute("SELECT question, pub_date FROM sanic_polls")
async for row in cur:
result.append({"question": row[0], "pub_date": row[1]})
return json({"polls": result})
if __name__ == '__main__':
loop.run_until_complete(prepare_db())
app.run(host='0.0.0.0', port=8000, loop=loop)

View File

@ -0,0 +1,73 @@
""" To run this example you need additional aiopg package
"""
import os
import asyncio
import datetime
import uvloop
from aiopg.sa import create_engine
import sqlalchemy as sa
from sanic import Sanic
from sanic.response import json
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
database_name = os.environ['DATABASE_NAME']
database_host = os.environ['DATABASE_HOST']
database_user = os.environ['DATABASE_USER']
database_password = os.environ['DATABASE_PASSWORD']
connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user,
database_password,
database_host,
database_name)
loop = asyncio.get_event_loop()
metadata = sa.MetaData()
polls = sa.Table('sanic_polls', metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('question', sa.String(50)),
sa.Column("pub_date", sa.DateTime))
async def get_engine():
return await create_engine(connection)
app = Sanic(name=__name__)
engine = loop.run_until_complete(get_engine())
async def prepare_db():
""" Let's add some data
"""
async with engine.acquire() as conn:
await conn.execute('DROP TABLE IF EXISTS sanic_polls')
await conn.execute("""CREATE TABLE sanic_polls (
id serial primary key,
question varchar(50),
pub_date timestamp
);""")
for i in range(0, 100):
await conn.execute(
polls.insert().values(question=i,
pub_date=datetime.datetime.now())
)
@app.route("/")
async def handle(request):
async with engine.acquire() as conn:
result = []
async for row in conn.execute(polls.select()):
result.append({"question": row.question, "pub_date": row.pub_date})
return json({"polls": result})
if __name__ == '__main__':
loop.run_until_complete(prepare_db())
app.run(host='0.0.0.0', port=8000, loop=loop)

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

View File

@ -2,6 +2,7 @@ httptools
ujson ujson
uvloop uvloop
aiohttp aiohttp
aiocache
pytest pytest
coverage coverage
tox tox
@ -10,3 +11,4 @@ bottle
kyoukai kyoukai
falcon falcon
tornado tornado
aiofiles

View File

@ -1,3 +1,5 @@
httptools httptools
ujson ujson
uvloop uvloop
aiofiles
multidict

View File

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

View File

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

View File

@ -91,6 +91,12 @@ class Blueprint:
return handler return handler
return decorator return decorator
def add_route(self, handler, uri, methods=None):
"""
"""
self.record(lambda s: s.add_route(handler, uri, methods))
return handler
def listener(self, event): def listener(self, event):
""" """
""" """
@ -109,8 +115,9 @@ class Blueprint:
# 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 len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
middleware = args[0]
args = [] args = []
return register_middleware(args[0]) return register_middleware(middleware)
else: else:
return register_middleware return register_middleware

View File

@ -30,6 +30,7 @@ def _quote(str):
else: else:
return '"' + str.translate(_Translator) + '"' return '"' + str.translate(_Translator) + '"'
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
# ------------------------------------------------------------ # # ------------------------------------------------------------ #

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
@ -30,6 +31,14 @@ class FileNotFound(NotFound):
self.relative_url = relative_url self.relative_url = relative_url
class RequestTimeout(SanicException):
status_code = 408
class PayloadTooLarge(SanicException):
status_code = 413
class Handler: class Handler:
handlers = None handlers = None
@ -48,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)
response = handler(request=request, exception=exception) try:
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

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

View File

@ -4,10 +4,17 @@ from http.cookies import SimpleCookie
from httptools import parse_url from httptools import parse_url
from urllib.parse import parse_qs from urllib.parse import parse_qs
from ujson import loads as json_loads from ujson import loads as json_loads
from sanic.exceptions import InvalidUsage
from .log import log from .log import log
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
# > If the media type remains unknown, the recipient SHOULD treat it
# > as 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
@ -26,7 +33,7 @@ class RequestParameters(dict):
return self.super.get(name, default) return self.super.get(name, default)
class Request: class Request(dict):
""" """
Properties of an HTTP request such as URL, headers, etc. Properties of an HTTP request such as URL, headers, etc.
""" """
@ -57,25 +64,35 @@ class Request:
@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:
pass raise InvalidUsage("Failed when parsing body as json")
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:
self.parsed_form = {} self.parsed_form = RequestParameters()
self.parsed_files = {} self.parsed_files = RequestParameters()
content_type, parameters = parse_header( content_type = self.headers.get(
self.headers.get('Content-Type')) 'Content-Type', DEFAULT_HTTP_CONTENT_TYPE)
content_type, parameters = parse_header(content_type)
try: try:
is_url_encoded = ( if content_type == 'application/x-www-form-urlencoded':
content_type == 'application/x-www-form-urlencoded')
if content_type is None or is_url_encoded:
self.parsed_form = RequestParameters( self.parsed_form = RequestParameters(
parse_qs(self.body.decode('utf-8'))) parse_qs(self.body.decode('utf-8')))
elif content_type == 'multipart/form-data': elif content_type == 'multipart/form-data':
@ -83,9 +100,8 @@ class Request:
boundary = parameters['boundary'].encode('utf-8') boundary = parameters['boundary'].encode('utf-8')
self.parsed_form, self.parsed_files = ( self.parsed_form, self.parsed_files = (
parse_multipart_form(self.body, boundary)) parse_multipart_form(self.body, boundary))
except Exception as e: except Exception:
log.exception(e) log.exception("Failed when parsing form")
pass
return self.parsed_form return self.parsed_form
@ -110,9 +126,10 @@ class Request:
@property @property
def cookies(self): def cookies(self):
if self._cookies is None: if self._cookies is None:
if 'Cookie' in self.headers: cookie = self.headers.get('Cookie') or self.headers.get('cookie')
if cookie is not None:
cookies = SimpleCookie() cookies = SimpleCookie()
cookies.load(self.headers['Cookie']) cookies.load(cookie)
self._cookies = {name: cookie.value self._cookies = {name: cookie.value
for name, cookie in cookies.items()} for name, cookie in cookies.items()}
else: else:
@ -128,10 +145,10 @@ def parse_multipart_form(body, boundary):
Parses 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 (dict), files (dict) :return: fields (RequestParameters), files (RequestParameters)
""" """
files = {} files = RequestParameters()
fields = {} fields = RequestParameters()
form_parts = body.split(boundary) form_parts = body.split(boundary)
for form_part in form_parts[1:-1]: for form_part in form_parts[1:-1]:
@ -162,9 +179,16 @@ def parse_multipart_form(body, boundary):
post_data = form_part[line_index:-4] post_data = form_part[line_index:-4]
if file_name or file_type: if file_name or file_type:
files[field_name] = File( file = File(type=file_type, name=file_name, body=post_data)
type=file_type, name=file_name, body=post_data) if field_name in files:
files[field_name].append(file)
else:
files[field_name] = [file]
else: else:
fields[field_name] = post_data.decode('utf-8') value = post_data.decode('utf-8')
if field_name in fields:
fields[field_name].append(value)
else:
fields[field_name] = [value]
return fields, files return fields, files

View File

@ -1,9 +1,11 @@
from aiofiles import open as open_async from aiofiles import open as open_async
from .cookies import CookieJar
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
from .cookies import CookieJar
COMMON_STATUS_CODES = { COMMON_STATUS_CODES = {
200: b'OK', 200: b'OK',
400: b'Bad Request', 400: b'Bad Request',
@ -79,7 +81,12 @@ class HTTPResponse:
self.content_type = content_type self.content_type = content_type
if body is not None: if body is not None:
self.body = body.encode('utf-8') try:
# Try to encode it regularly
self.body = body.encode('utf-8')
except AttributeError:
# Convert it to a str if you can't
self.body = str(body).encode('utf-8')
else: else:
self.body = body_bytes self.body = body_bytes
@ -96,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

@ -23,18 +23,28 @@ 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...
or
@app.route('/my_url/<my_param:my_type>', methods=['GET', 'POST', ...])
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
function provided Parameters can also have a type by appending :type to function. Provided parameters can also have a type by appending :type to
the <parameter>. If no type is provided, a string is expected. A regular the <parameter>. Given parameter must be able to be type-casted to this.
expression can also be passed in as the type If no type is provided, a string is expected. A regular expression can
also be passed in as the type. The argument given to the function will
always be a string, independent of the type.
""" """
routes_static = None routes_static = None
routes_dynamic = None routes_dynamic = None
@ -103,6 +113,23 @@ class Router:
else: else:
self.routes_static[uri] = route self.routes_static[uri] = route
def remove(self, uri, clean_cache=True):
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

View File

@ -1,24 +1,35 @@
from asyncio import get_event_loop from asyncio import get_event_loop
from collections import deque from collections import deque
from functools import partial from functools import partial
from inspect import isawaitable from inspect import isawaitable, stack, getmodulename
from multiprocessing import Process, Event from multiprocessing import Process, Event
from signal import signal, SIGTERM, SIGINT from signal import signal, SIGTERM, SIGINT
from time import sleep
from traceback import format_exc from traceback import format_exc
import logging
from .config import Config from .config import Config
from .exceptions import Handler from .exceptions import Handler
from .log import log, logging from .log import log
from .response import HTTPResponse from .response import HTTPResponse
from .router import Router from .router import Router
from .server import serve from .server import serve, HttpProtocol
from .static import register as static_register from .static import register as static_register
from .exceptions import ServerError from .exceptions import ServerError
from socket import socket, SOL_SOCKET, SO_REUSEADDR
from os import set_inheritable
class Sanic: class Sanic:
def __init__(self, name, router=None, error_handler=None): def __init__(self, name=None, router=None,
error_handler=None, logger=None):
if logger is None:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s: %(levelname)s: %(message)s"
)
if name is None:
frame_records = stack()[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 Handler(self) self.error_handler = error_handler or Handler(self)
@ -29,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
@ -57,6 +70,22 @@ class Sanic:
return response return response
def add_route(self, handler, uri, methods=None):
"""
A helper method to register class instance or
functions as a handler to the application url
routes.
:param handler: function or class instance
:param uri: path of the URL
:param methods: list or tuple of methods allowed
:return: function or class instance
"""
self.route(uri=uri, methods=methods)(handler)
return handler
def remove_route(self, uri, clean_cache=True):
self.router.remove(uri, clean_cache)
# Decorator # Decorator
def exception(self, *exceptions): def exception(self, *exceptions):
""" """
@ -177,18 +206,18 @@ class Sanic:
if isawaitable(response): if isawaitable(response):
response = await response response = await response
# -------------------------------------------- # # -------------------------------------------- #
# Response Middleware # Response Middleware
# -------------------------------------------- # # -------------------------------------------- #
if self.response_middleware: if self.response_middleware:
for middleware in self.response_middleware: for middleware in self.response_middleware:
_response = middleware(request, response) _response = middleware(request, response)
if isawaitable(_response): if isawaitable(_response):
_response = await _response _response = await _response
if _response: if _response:
response = _response response = _response
break break
except Exception as e: except Exception as e:
# -------------------------------------------- # # -------------------------------------------- #
@ -216,25 +245,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
@ -242,14 +273,17 @@ 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,
'debug': debug, 'debug': debug,
'request_handler': self.handle_request, 'request_handler': self.handle_request,
'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
} }
# -------------------------------------------- # # -------------------------------------------- #
@ -266,7 +300,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:
@ -288,12 +322,11 @@ 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(
'Experienced exception while trying to serve: {}'.format(e)) 'Experienced exception while trying to serve')
pass
log.info("Server Stopped") log.info("Server Stopped")
@ -301,10 +334,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.
@ -316,26 +352,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

@ -1,8 +1,12 @@
import asyncio import asyncio
from functools import partial
from inspect import isawaitable from inspect import isawaitable
from multidict import CIMultiDict
from signal import SIGINT, SIGTERM from signal import SIGINT, SIGTERM
from time import time
import httptools from httptools import HttpRequestParser
from httptools.parser.errors import HttpParserError
from .exceptions import ServerError
try: try:
import uvloop as async_loop import uvloop as async_loop
@ -11,12 +15,16 @@ except ImportError:
from .log import log from .log import log
from .request import Request from .request import Request
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
class Signal: class Signal:
stopped = False stopped = False
current_time = None
class HttpProtocol(asyncio.Protocol): class HttpProtocol(asyncio.Protocol):
__slots__ = ( __slots__ = (
# event loop, connection # event loop, connection
@ -26,10 +34,10 @@ class HttpProtocol(asyncio.Protocol):
# request config # request config
'request_handler', 'request_timeout', 'request_max_size', 'request_handler', 'request_timeout', 'request_max_size',
# connection management # connection management
'_total_request_size', '_timeout_handler') '_total_request_size', '_timeout_handler', '_last_communication_time')
def __init__(self, *, loop, request_handler, signal=Signal(), def __init__(self, *, loop, request_handler, error_handler,
connections={}, request_timeout=60, signal=Signal(), connections={}, request_timeout=60,
request_max_size=None): request_max_size=None):
self.loop = loop self.loop = loop
self.transport = None self.transport = None
@ -40,32 +48,44 @@ class HttpProtocol(asyncio.Protocol):
self.signal = signal self.signal = signal
self.connections = connections self.connections = connections
self.request_handler = request_handler self.request_handler = request_handler
self.error_handler = error_handler
self.request_timeout = request_timeout self.request_timeout = request_timeout
self.request_max_size = request_max_size self.request_max_size = request_max_size
self._total_request_size = 0 self._total_request_size = 0
self._timeout_handler = None self._timeout_handler = None
self._last_request_time = None
self._request_handler_task = None
# -------------------------------------------- # # -------------------------------------------- #
# Connection # Connection
# -------------------------------------------- # # -------------------------------------------- #
def connection_made(self, transport): def connection_made(self, transport):
self.connections[self] = True self.connections.add(self)
self._timeout_handler = self.loop.call_later( self._timeout_handler = self.loop.call_later(
self.request_timeout, self.connection_timeout) self.request_timeout, self.connection_timeout)
self.transport = transport self.transport = transport
self._last_request_time = current_time
def connection_lost(self, exc): def connection_lost(self, exc):
del self.connections[self] self.connections.discard(self)
self._timeout_handler.cancel() self._timeout_handler.cancel()
self.cleanup() self.cleanup()
def connection_timeout(self): def connection_timeout(self):
self.bail_out("Request timed out, connection closed") # Check if
time_elapsed = current_time - self._last_request_time
# -------------------------------------------- # if time_elapsed < self.request_timeout:
time_left = self.request_timeout - time_elapsed
self._timeout_handler = \
self.loop.call_later(time_left, self.connection_timeout)
else:
if self._request_handler_task:
self._request_handler_task.cancel()
exception = RequestTimeout('Request Timeout')
self.write_error(exception)
# -------------------------------------------- #
# Parsing # Parsing
# -------------------------------------------- # # -------------------------------------------- #
@ -74,37 +94,40 @@ class HttpProtocol(asyncio.Protocol):
# memory limits # memory limits
self._total_request_size += len(data) self._total_request_size += len(data)
if self._total_request_size > self.request_max_size: if self._total_request_size > self.request_max_size:
return self.bail_out( exception = PayloadTooLarge('Payload Too Large')
"Request too large ({}), connection closed".format( self.write_error(exception)
self._total_request_size))
# Create parser if this is the first time we're receiving data # Create parser if this is the first time we're receiving data
if self.parser is None: if self.parser is None:
assert self.request is None assert self.request is None
self.headers = [] self.headers = []
self.parser = httptools.HttpRequestParser(self) self.parser = HttpRequestParser(self)
# Parse request chunk or close connection # Parse request chunk or close connection
try: try:
self.parser.feed_data(data) self.parser.feed_data(data)
except httptools.parser.errors.HttpParserError as e: except HttpParserError:
self.bail_out( exception = InvalidUsage('Bad Request')
"Invalid request data, connection closed ({})".format(e)) self.write_error(exception)
def on_url(self, url): def on_url(self, url):
self.url = url self.url = url
def on_header(self, name, value): def on_header(self, name, value):
if name == b'Content-Length' and int(value) > self.request_max_size: if name == b'Content-Length' and int(value) > self.request_max_size:
return self.bail_out( exception = PayloadTooLarge('Payload Too Large')
"Request body too large ({}), connection closed".format(value)) self.write_error(exception)
self.headers.append((name.decode(), value.decode('utf-8'))) self.headers.append((name.decode(), value.decode('utf-8')))
def on_headers_complete(self): def on_headers_complete(self):
remote_addr = self.transport.get_extra_info('peername')
if remote_addr:
self.headers.append(('Remote-Addr', '%s:%s' % remote_addr))
self.request = Request( self.request = Request(
url_bytes=self.url, url_bytes=self.url,
headers=dict(self.headers), headers=CIMultiDict(self.headers),
version=self.parser.get_http_version(), version=self.parser.get_http_version(),
method=self.parser.get_method().decode() method=self.parser.get_method().decode()
) )
@ -116,7 +139,7 @@ class HttpProtocol(asyncio.Protocol):
self.request.body = body self.request.body = body
def on_message_complete(self): def on_message_complete(self):
self.loop.create_task( self._request_handler_task = self.loop.create_task(
self.request_handler(self.request, self.write_response)) self.request_handler(self.request, self.write_response))
# -------------------------------------------- # # -------------------------------------------- #
@ -133,20 +156,34 @@ class HttpProtocol(asyncio.Protocol):
if not keep_alive: if not keep_alive:
self.transport.close() self.transport.close()
else: else:
# Record that we received data
self._last_request_time = current_time
self.cleanup() self.cleanup()
except Exception as e: except Exception as e:
self.bail_out( self.bail_out(
"Writing request failed, connection closed {}".format(e)) "Writing response failed, connection closed {}".format(e))
def write_error(self, exception):
try:
response = self.error_handler.response(self.request, exception)
version = self.request.version if self.request else '1.1'
self.transport.write(response.output(version))
self.transport.close()
except Exception as e:
self.bail_out(
"Writing error failed, connection closed {}".format(e))
def bail_out(self, message): def bail_out(self, message):
exception = ServerError(message)
self.write_error(exception)
log.error(message) log.error(message)
self.transport.close()
def cleanup(self): def cleanup(self):
self.parser = None self.parser = None
self.request = None self.request = None
self.url = None self.url = None
self.headers = None self.headers = None
self._request_handler_task = None
self._total_request_size = 0 self._total_request_size = 0
def close_if_idle(self): def close_if_idle(self):
@ -160,6 +197,18 @@ class HttpProtocol(asyncio.Protocol):
return False return False
def update_current_time(loop):
"""
Caches the current time, since it is needed
at the end of every keep-alive request to update the request timeout time
:param loop:
:return:
"""
global current_time
current_time = time()
loop.call_later(1, partial(update_current_time, loop))
def trigger_events(events, loop): def trigger_events(events, loop):
""" """
:param events: one or more sync or async functions to execute :param events: one or more sync or async functions to execute
@ -174,25 +223,31 @@ def trigger_events(events, loop):
loop.run_until_complete(result) loop.run_until_complete(result)
def serve(host, port, request_handler, before_start=None, after_start=None, def serve(host, port, request_handler, error_handler, before_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()
@ -203,16 +258,31 @@ def serve(host, port, request_handler, before_start=None, after_start=None,
trigger_events(before_start, loop) trigger_events(before_start, loop)
connections = {} connections = set()
signal = Signal() signal = Signal()
server_coroutine = loop.create_server(lambda: HttpProtocol( server = partial(
protocol,
loop=loop, loop=loop,
connections=connections, connections=connections,
signal=signal, signal=signal,
request_handler=request_handler, request_handler=request_handler,
error_handler=error_handler,
request_timeout=request_timeout, request_timeout=request_timeout,
request_max_size=request_max_size, request_max_size=request_max_size,
), host, port, reuse_port=reuse_port, sock=sock) )
server_coroutine = loop.create_server(
server,
host,
port,
reuse_port=reuse_port,
sock=sock,
backlog=backlog
)
# Instead of pulling time at the end of every request,
# pull it once per minute
loop.call_soon(partial(update_current_time, loop))
try: try:
http_server = loop.run_until_complete(server_coroutine) http_server = loop.run_until_complete(server_coroutine)
@ -240,7 +310,7 @@ def serve(host, port, request_handler, before_start=None, after_start=None,
# Complete all tasks on the loop # Complete all tasks on the loop
signal.stopped = True signal.stopped = True
for connection in connections.keys(): for connection in connections:
connection.close_if_idle() connection.close_if_idle()
while connections: while connections:

View File

@ -2,6 +2,7 @@ from aiofiles.os import stat
from os import path from os import path
from re import sub from re import sub
from time import strftime, gmtime from time import strftime, gmtime
from urllib.parse import unquote
from .exceptions import FileNotFound, InvalidUsage from .exceptions import FileNotFound, InvalidUsage
from .response import file, HTTPResponse from .response import file, HTTPResponse
@ -32,12 +33,17 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
# served. os.path.realpath seems to be very slow # served. os.path.realpath seems to be very slow
if file_uri and '../' in file_uri: if file_uri and '../' in file_uri:
raise InvalidUsage("Invalid URL") raise InvalidUsage("Invalid URL")
# Merge served directory and requested file if provided # Merge served directory and requested file if provided
# Strip all / that in the beginning of the URL to help prevent python # Strip all / that in the beginning of the URL to help prevent python
# from herping a derp and treating the uri as an absolute path # from herping a derp and treating the uri as an absolute path
file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ file_path = file_or_directory
if file_uri else file_or_directory if file_uri:
file_path = path.join(
file_or_directory, sub('^[/]*', '', file_uri))
# URL decode the path sent by the browser otherwise we won't be able to
# match filenames which got encoded (filenames with spaces etc)
file_path = unquote(file_path)
try: try:
headers = {} headers = {}
# Check if the client has been sent this file before # Check if the client has been sent this file before

View File

@ -16,14 +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, *request_args, **request_kwargs): loop=None, debug=False, server_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:
@ -34,7 +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, port=42101, after_start=_collect_response, loop=loop) app.run(host=HOST, debug=debug, port=PORT,
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))
@ -45,11 +47,11 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
return request, response return request, response
except: except:
raise ValueError( raise ValueError(
"request and response object expected, got ({})".format( "Request and response object expected, got ({})".format(
results)) results))
else: else:
try: try:
return results[0] return results[0]
except: except:
raise ValueError( raise ValueError(
"request object expected, got ({})".format(results)) "Request object expected, got ({})".format(results))

63
sanic/views.py Normal file
View File

@ -0,0 +1,63 @@
from .exceptions import InvalidUsage
class HTTPMethodView:
""" Simple class based implementation of view for the sanic.
You should implement methods (get, post, put, patch, delete) for the class
to every HTTP method you want to support.
For example:
class DummyView(HTTPMethodView):
def get(self, request, *args, **kwargs):
return text('I am get method')
def put(self, request, *args, **kwargs):
return text('I am put method')
etc.
If someone tries to use a non-implemented method, there will be a
405 response.
If you need any url params just mention them in method definition:
class DummyView(HTTPMethodView):
def get(self, request, my_param_here, *args, **kwargs):
return text('I am get method with %s' % my_param_here)
To add the view into the routing you could use
1) app.add_route(DummyView.as_view(), '/')
2) app.route('/')(DummyView.as_view())
To add any decorator you could set it into decorators variable
"""
decorators = []
def dispatch_request(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None)
if handler:
return handler(request, *args, **kwargs)
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
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

@ -30,6 +30,7 @@ setup(
'httptools>=0.0.9', 'httptools>=0.0.9',
'ujson>=1.35', 'ujson>=1.35',
'aiofiles>=0.3.0', 'aiofiles>=0.3.0',
'multidict>=2.0',
], ],
classifiers=[ classifiers=[
'Development Status :: 2 - Pre-Alpha', 'Development Status :: 2 - Pre-Alpha',

View File

@ -15,4 +15,4 @@ async def handle(request):
app = web.Application(loop=loop) app = web.Application(loop=loop)
app.router.add_route('GET', '/', handle) app.router.add_route('GET', '/', handle)
web.run_app(app, port=sys.argv[1]) web.run_app(app, port=sys.argv[1], access_log=None)

View File

@ -1,16 +1,30 @@
package main package main
import ( import (
"fmt" "encoding/json"
"os" "net/http"
"net/http" "os"
) )
type TestJSONResponse struct {
Test bool
}
func handler(w http.ResponseWriter, r *http.Request) { func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:]) response := TestJSONResponse{true}
js, err := json.Marshal(response)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(js)
} }
func main() { func main() {
http.HandleFunc("/", handler) http.HandleFunc("/", handler)
http.ListenAndServe(":" + os.Args[1], nil) http.ListenAndServe(":"+os.Args[1], nil)
} }

View File

@ -0,0 +1 @@
I need to be decoded as a uri

1
tests/static/test.file Normal file
View File

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

20
tests/test_bad_request.py Normal file
View File

@ -0,0 +1,20 @@
import asyncio
from sanic import Sanic
def test_bad_request_response():
app = Sanic('test_bad_request_response')
lines = []
async def _request(sanic, loop):
connect = asyncio.open_connection('127.0.0.1', 42101)
reader, writer = await connect
writer.write(b'not http')
while True:
line = await reader.readline()
if not line:
break
lines.append(line)
app.stop()
app.run(host='127.0.0.1', port=42101, debug=False, after_start=_request)
assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n'
assert lines[-1] == b'Error: Bad Request'

View File

@ -25,6 +25,19 @@ def test_cookies():
assert response.text == 'Cookies are: working!' assert response.text == 'Cookies are: working!'
assert response_cookies['right_back'].value == 'at you' assert response_cookies['right_back'].value == 'at you'
def test_http2_cookies():
app = Sanic('test_http2_cookies')
@app.route('/')
async def handler(request):
response = text('Cookies are: {}'.format(request.cookies['test']))
return response
headers = {'cookie': 'test=working!'}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == 'Cookies are: working!'
def test_cookie_options(): def test_cookie_options():
app = Sanic('test_text') app = Sanic('test_text')

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 handler(request): def exception_app():
return text('OK') app = Sanic('test_exceptions')
@app.route('/')
def handler(request):
return text('OK')
@app.route('/error')
def handler_error(request):
raise ServerError("OK")
@app.route('/404')
def handler_404(request):
raise NotFound("OK")
@app.route('/invalid')
def handler_invalid(request):
raise InvalidUsage("OK")
@app.route('/divide_by_zero')
def handle_unhandled_exception(request):
1 / 0
@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
@exception_app.route('/error') def test_no_exception(exception_app):
def handler_error(request): """Test that a route works without an exception"""
raise ServerError("OK")
@exception_app.route('/404')
def handler_404(request):
raise NotFound("OK")
@exception_app.route('/invalid')
def handler_invalid(request):
raise InvalidUsage("OK")
def test_no_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'

33
tests/test_logging.py Normal file
View File

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

View File

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

View File

@ -0,0 +1,54 @@
from sanic import Sanic
from sanic.response import text
from sanic.exceptions import PayloadTooLarge
from sanic.utils import sanic_endpoint_test
data_received_app = Sanic('data_received')
data_received_app.config.REQUEST_MAX_SIZE = 1
data_received_default_app = Sanic('data_received_default')
data_received_default_app.config.REQUEST_MAX_SIZE = 1
on_header_default_app = Sanic('on_header')
on_header_default_app.config.REQUEST_MAX_SIZE = 500
@data_received_app.route('/1')
async def handler1(request):
return text('OK')
@data_received_app.exception(PayloadTooLarge)
def handler_exception(request, exception):
return text('Payload Too Large from error_handler.', 413)
def test_payload_too_large_from_error_handler():
response = sanic_endpoint_test(
data_received_app, uri='/1', gather_request=False)
assert response.status == 413
assert response.text == 'Payload Too Large from error_handler.'
@data_received_default_app.route('/1')
async def handler2(request):
return text('OK')
def test_payload_too_large_at_data_received_default():
response = sanic_endpoint_test(
data_received_default_app, uri='/1', gather_request=False)
assert response.status == 413
assert response.text == 'Error: Payload Too Large'
@on_header_default_app.route('/1')
async def handler3(request):
return text('OK')
def test_payload_too_large_at_on_header_default():
data = 'a' * 1000
response = sanic_endpoint_test(
on_header_default_app, method='post', uri='/1',
gather_request=False, data=data)
assert response.status == 413
assert response.text == 'Error: Payload Too Large'

View File

@ -0,0 +1,24 @@
from sanic import Sanic
from sanic.response import json
from sanic.utils import sanic_endpoint_test
from ujson import loads
def test_storage():
app = Sanic('test_text')
@app.middleware('request')
def store(request):
request['user'] = 'sanic'
request['sidekick'] = 'tails'
del request['sidekick']
@app.route('/')
def handler(request):
return json({ 'user': request.get('user'), 'sidekick': request.get('sidekick') })
request, response = sanic_endpoint_test(app)
response_json = loads(response.text)
assert response_json['user'] == 'sanic'
assert response_json.get('sidekick') is None

View File

@ -0,0 +1,40 @@
from sanic import Sanic
import asyncio
from sanic.response import text
from sanic.exceptions import RequestTimeout
from sanic.utils import sanic_endpoint_test
from sanic.config import Config
Config.REQUEST_TIMEOUT = 1
request_timeout_app = Sanic('test_request_timeout')
request_timeout_default_app = Sanic('test_request_timeout_default')
@request_timeout_app.route('/1')
async def handler_1(request):
await asyncio.sleep(2)
return text('OK')
@request_timeout_app.exception(RequestTimeout)
def handler_exception(request, exception):
return text('Request Timeout from error_handler.', 408)
def test_server_error_request_timeout():
request, response = sanic_endpoint_test(request_timeout_app, uri='/1')
assert response.status == 408
assert response.text == 'Request Timeout from error_handler.'
@request_timeout_default_app.route('/1')
async def handler_2(request):
await asyncio.sleep(2)
return text('OK')
def test_default_server_error_request_timeout():
request, response = sanic_endpoint_test(
request_timeout_default_app, uri='/1')
assert response.status == 408
assert response.text == 'Error: Request Timeout'

View File

@ -2,6 +2,7 @@ from json import loads as json_loads, dumps as json_dumps
from sanic import Sanic from sanic import Sanic
from sanic.response import json, text from sanic.response import json, text
from sanic.utils import sanic_endpoint_test from sanic.utils import sanic_endpoint_test
from sanic.exceptions import ServerError
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
@ -32,6 +33,47 @@ 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():
app = Sanic('test_invalid_response')
@app.exception(ServerError)
def handler_exception(request, exception):
return text('Internal Server Error.', 500)
@app.route('/')
async def handler(request):
return 'This should fail'
request, response = sanic_endpoint_test(app)
assert response.status == 500
assert response.text == "Internal Server Error."
def test_json(): def test_json():
app = Sanic('test_json') app = Sanic('test_json')
@ -49,6 +91,19 @@ def test_json():
assert results.get('test') == True assert results.get('test') == True
def test_invalid_json():
app = Sanic('test_json')
@app.route('/')
async def handler(request):
return json(request.json())
data = "I am not json"
request, response = sanic_endpoint_test(app, data=data)
assert response.status == 400
def test_query_string(): def test_query_string():
app = Sanic('test_query_string') app = Sanic('test_query_string')
@ -56,12 +111,30 @@ def test_query_string():
async def handler(request): async def handler(request):
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app, params=[("test1", 1), ("test2", "false"), ("test2", "true")]) request, response = sanic_endpoint_test(app, params=[("test1", "1"), ("test2", "false"), ("test2", "true")])
assert request.args.get('test1') == '1' assert request.args.get('test1') == '1'
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
# ------------------------------------------------------------ # # ------------------------------------------------------------ #

18
tests/test_response.py Normal file
View File

@ -0,0 +1,18 @@
from random import choice
from sanic import Sanic
from sanic.response import HTTPResponse
from sanic.utils import sanic_endpoint_test
def test_response_body_not_a_string():
"""Test when a response body sent from the application is not a string"""
app = Sanic('response_body_not_a_string')
random_num = choice(range(1000))
@app.route('/hello')
async def hello_route(request):
return HTTPResponse(body=random_num)
request, response = sanic_endpoint_test(app, uri='/hello')
assert response.text == str(random_num)

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
@ -84,7 +84,7 @@ def test_dynamic_route_int():
def test_dynamic_route_number(): def test_dynamic_route_number():
app = Sanic('test_dynamic_route_int') app = Sanic('test_dynamic_route_number')
results = [] results = []
@ -105,7 +105,7 @@ def test_dynamic_route_number():
def test_dynamic_route_regex(): def test_dynamic_route_regex():
app = Sanic('test_dynamic_route_int') app = Sanic('test_dynamic_route_regex')
@app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>') @app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>')
async def handler(request, folder_id): async def handler(request, folder_id):
@ -145,7 +145,7 @@ def test_dynamic_route_unhashable():
def test_route_duplicate(): def test_route_duplicate():
app = Sanic('test_dynamic_route') app = Sanic('test_route_duplicate')
with pytest.raises(RouteExists): with pytest.raises(RouteExists):
@app.route('/test') @app.route('/test')
@ -178,3 +178,288 @@ def test_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_static_add_route():
app = Sanic('test_static_add_route')
async def handler1(request):
return text('OK1')
async def handler2(request):
return text('OK2')
app.add_route(handler1, '/test')
app.add_route(handler2, '/test2')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, uri='/test2')
assert response.text == 'OK2'
def test_dynamic_add_route():
app = Sanic('test_dynamic_add_route')
results = []
async def handler(request, name):
results.append(name)
return text('OK')
app.add_route(handler, '/folder/<name>')
request, response = sanic_endpoint_test(app, uri='/folder/test123')
assert response.text == 'OK'
assert results[0] == 'test123'
def test_dynamic_add_route_string():
app = Sanic('test_dynamic_add_route_string')
results = []
async def handler(request, name):
results.append(name)
return text('OK')
app.add_route(handler, '/folder/<name:string>')
request, response = sanic_endpoint_test(app, uri='/folder/test123')
assert response.text == 'OK'
assert results[0] == 'test123'
request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico')
assert response.text == 'OK'
assert results[1] == 'favicon.ico'
def test_dynamic_add_route_int():
app = Sanic('test_dynamic_add_route_int')
results = []
async def handler(request, folder_id):
results.append(folder_id)
return text('OK')
app.add_route(handler, '/folder/<folder_id:int>')
request, response = sanic_endpoint_test(app, uri='/folder/12345')
assert response.text == 'OK'
assert type(results[0]) is int
request, response = sanic_endpoint_test(app, uri='/folder/asdf')
assert response.status == 404
def test_dynamic_add_route_number():
app = Sanic('test_dynamic_add_route_number')
results = []
async def handler(request, weight):
results.append(weight)
return text('OK')
app.add_route(handler, '/weight/<weight:number>')
request, response = sanic_endpoint_test(app, uri='/weight/12345')
assert response.text == 'OK'
assert type(results[0]) is float
request, response = sanic_endpoint_test(app, uri='/weight/1234.56')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/weight/1234-56')
assert response.status == 404
def test_dynamic_add_route_regex():
app = Sanic('test_dynamic_route_int')
async def handler(request, folder_id):
return text('OK')
app.add_route(handler, '/folder/<folder_id:[A-Za-z0-9]{0,4}>')
request, response = sanic_endpoint_test(app, uri='/folder/test')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test1')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/test-123')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/')
assert response.status == 200
def test_dynamic_add_route_unhashable():
app = Sanic('test_dynamic_add_route_unhashable')
async def handler(request, unhashable):
return text('OK')
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/')
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
request, response = sanic_endpoint_test(app, uri='/folder/test/nope/')
assert response.status == 404
def test_add_route_duplicate():
app = Sanic('test_add_route_duplicate')
with pytest.raises(RouteExists):
async def handler1(request):
pass
async def handler2(request):
pass
app.add_route(handler1, '/test')
app.add_route(handler2, '/test')
with pytest.raises(RouteExists):
async def handler1(request, dynamic):
pass
async def handler2(request, dynamic):
pass
app.add_route(handler1, '/test/<dynamic>/')
app.add_route(handler2, '/test/<dynamic>/')
def test_add_route_method_not_allowed():
app = Sanic('test_add_route_method_not_allowed')
async def handler(request):
return text('OK')
app.add_route(handler, '/test', methods=['GET'])
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
request, response = sanic_endpoint_test(app, method='post', uri='/test')
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

@ -1,30 +1,62 @@
import inspect import inspect
import os import os
import pytest
from sanic import Sanic from sanic import Sanic
from sanic.utils import sanic_endpoint_test from sanic.utils import sanic_endpoint_test
def test_static_file():
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, 'rb') as file:
current_file_contents = file.read()
@pytest.fixture(scope='module')
def static_file_directory():
"""The static directory to serve"""
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
static_directory = os.path.join(current_directory, 'static')
return static_directory
@pytest.fixture(scope='module')
def static_file_path(static_file_directory):
"""The path to the static file that we want to serve"""
return os.path.join(static_file_directory, 'test.file')
@pytest.fixture(scope='module')
def static_file_content(static_file_path):
"""The content of the static file to check"""
with open(static_file_path, 'rb') as file:
return file.read()
def test_static_file(static_file_path, static_file_content):
app = Sanic('test_static') app = Sanic('test_static')
app.static('/testing.file', current_file) app.static('/testing.file', static_file_path)
request, response = sanic_endpoint_test(app, uri='/testing.file') request, response = sanic_endpoint_test(app, uri='/testing.file')
assert response.status == 200 assert response.status == 200
assert response.body == current_file_contents assert response.body == static_file_content
def test_static_directory():
current_file = inspect.getfile(inspect.currentframe()) def test_static_directory(
current_directory = os.path.dirname(os.path.abspath(current_file)) static_file_directory, static_file_path, static_file_content):
with open(current_file, 'rb') as file:
current_file_contents = file.read()
app = Sanic('test_static') app = Sanic('test_static')
app.static('/dir', current_directory) app.static('/dir', static_file_directory)
request, response = sanic_endpoint_test(app, uri='/dir/test_static.py') request, response = sanic_endpoint_test(app, uri='/dir/test.file')
assert response.status == 200 assert response.status == 200
assert response.body == current_file_contents assert response.body == static_file_content
def test_static_url_decode_file(static_file_directory):
decode_me_path = os.path.join(static_file_directory, 'decode me.txt')
with open(decode_me_path, 'rb') as file:
decode_me_contents = file.read()
app = Sanic('test_static')
app.static('/dir', static_file_directory)
request, response = sanic_endpoint_test(app, uri='/dir/decode me.txt')
assert response.status == 200
assert response.body == decode_me_contents

196
tests/test_views.py Normal file
View File

@ -0,0 +1,196 @@
from sanic import Sanic
from sanic.response import text, HTTPResponse
from sanic.views import HTTPMethodView
from sanic.blueprints import Blueprint
from sanic.request import Request
from sanic.utils import sanic_endpoint_test
def test_methods():
app = Sanic('test_methods')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
def post(self, request):
return text('I am post method')
def put(self, request):
return text('I am put method')
def patch(self, request):
return text('I am patch method')
def delete(self, request):
return text('I am delete method')
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method'
request, response = sanic_endpoint_test(app, method="post")
assert response.text == 'I am post method'
request, response = sanic_endpoint_test(app, method="put")
assert response.text == 'I am put method'
request, response = sanic_endpoint_test(app, method="patch")
assert response.text == 'I am patch method'
request, response = sanic_endpoint_test(app, method="delete")
assert response.text == 'I am delete method'
def test_unexisting_methods():
app = Sanic('test_unexisting_methods')
class DummyView(HTTPMethodView):
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'
request, response = sanic_endpoint_test(app, method="post")
assert response.text == 'Error: Method POST not allowed for URL /'
def test_argument_methods():
app = Sanic('test_argument_methods')
class DummyView(HTTPMethodView):
def get(self, request, my_param_here):
return text('I am get method with %s' % my_param_here)
app.add_route(DummyView.as_view(), '/<my_param_here>')
request, response = sanic_endpoint_test(app, uri='/test123')
assert response.text == 'I am get method with test123'
def test_with_bp():
app = Sanic('test_with_bp')
bp = Blueprint('test_text')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
bp.add_route(DummyView.as_view(), '/')
app.blueprint(bp)
request, response = sanic_endpoint_test(app)
assert response.text == 'I am get method'
def test_with_bp_with_url_prefix():
app = Sanic('test_with_bp_with_url_prefix')
bp = Blueprint('test_text', url_prefix='/test1')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
bp.add_route(DummyView.as_view(), '/')
app.blueprint(bp)
request, response = sanic_endpoint_test(app, uri='/test1/')
assert response.text == 'I am get method'
def test_with_middleware():
app = Sanic('test_with_middleware')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
app.add_route(DummyView.as_view(), '/')
results = []
@app.middleware
async def handler(request):
results.append(request)
request, response = sanic_endpoint_test(app)
assert response.text == 'I am get method'
assert type(results[0]) is Request
def test_with_middleware_response():
app = Sanic('test_with_middleware_response')
results = []
@app.middleware('request')
async def process_response(request):
results.append(request)
@app.middleware('response')
async def process_response(request, response):
results.append(request)
results.append(response)
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app)
assert response.text == 'I am get method'
assert type(results[0]) is Request
assert type(results[1]) is Request
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

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

33
tox.ini
View File

@ -1,34 +1,25 @@
[tox] [tox]
envlist = py35, report envlist = py35, py36, flake8
[travis]
python =
3.5: py35, flake8
3.6: py36, flake8
[testenv] [testenv]
deps = deps =
aiohttp aiohttp
pytest pytest
# pytest-cov
coverage
commands = commands =
coverage run -m pytest tests {posargs} pytest tests {posargs}
mv .coverage .coverage.{envname}
basepython: [testenv:flake8]
py35: python3.5 deps =
flake8
whitelist_externals =
coverage
mv
echo
[testenv:report]
commands = commands =
coverage combine flake8 sanic
coverage report
coverage html
echo "Open file://{toxinidir}/coverage/index.html"
basepython =
python3.5