Merge branch 'master' into sphinx-docs
This commit is contained in:
commit
9d4b104d2d
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,13 +1,15 @@
|
||||||
*~
|
*~
|
||||||
*.egg-info
|
*.egg-info
|
||||||
*.egg
|
*.egg
|
||||||
|
*.eggs
|
||||||
|
*.pyc
|
||||||
.coverage
|
.coverage
|
||||||
.coverage.*
|
.coverage.*
|
||||||
coverage
|
coverage
|
||||||
.tox
|
.tox
|
||||||
settings.py
|
settings.py
|
||||||
*.pyc
|
|
||||||
.idea/*
|
.idea/*
|
||||||
.cache/*
|
.cache/*
|
||||||
|
.python-version
|
||||||
docs/_build/
|
docs/_build/
|
||||||
docs/_api/
|
docs/_api/
|
11
.travis.yml
11
.travis.yml
|
@ -1,15 +1,10 @@
|
||||||
|
sudo: false
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- '3.5'
|
- '3.5'
|
||||||
- '3.6'
|
- '3.6'
|
||||||
install:
|
install: pip install tox-travis
|
||||||
- pip install -r requirements.txt
|
script: tox
|
||||||
- pip install -r requirements-dev.txt
|
|
||||||
- python setup.py install
|
|
||||||
- pip install flake8
|
|
||||||
- pip install pytest
|
|
||||||
before_script: flake8 sanic
|
|
||||||
script: py.test -v tests
|
|
||||||
deploy:
|
deploy:
|
||||||
provider: pypi
|
provider: pypi
|
||||||
user: channelcat
|
user: channelcat
|
||||||
|
|
34
README.rst
34
README.rst
|
@ -74,3 +74,37 @@ Documentation can be found in the ``docs`` directory.
|
||||||
:target: https://pypi.python.org/pypi/sanic/
|
:target: https://pypi.python.org/pypi/sanic/
|
||||||
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
|
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
|
||||||
:target: https://pypi.python.org/pypi/sanic/
|
:target: https://pypi.python.org/pypi/sanic/
|
||||||
|
|
||||||
|
TODO
|
||||||
|
----
|
||||||
|
* Streamed file processing
|
||||||
|
* File output
|
||||||
|
* Examples of integrations with 3rd-party modules
|
||||||
|
* RESTful router
|
||||||
|
|
||||||
|
Limitations
|
||||||
|
-----------
|
||||||
|
* No wheels for uvloop and httptools on Windows :(
|
||||||
|
|
||||||
|
Final Thoughts
|
||||||
|
--------------
|
||||||
|
|
||||||
|
▄▄▄▄▄
|
||||||
|
▀▀▀██████▄▄▄ _______________
|
||||||
|
▄▄▄▄▄ █████████▄ / \
|
||||||
|
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
||||||
|
▀▀█████▄▄ ▀██████▄██ | _________________/
|
||||||
|
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
||||||
|
▀▀▀▄ ▀▀███ ▀ ▄▄
|
||||||
|
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
|
||||||
|
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
|
||||||
|
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
|
||||||
|
▌ ▐▀████▐███▒▒▒▒▒▐██▌
|
||||||
|
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
|
||||||
|
▀▀█████████▀
|
||||||
|
▄▄██▀██████▀█
|
||||||
|
▄██▀ ▀▀▀ █
|
||||||
|
▄█ ▐▌
|
||||||
|
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
|
||||||
|
▌ ▐ ▀▀▄▄▄▀
|
||||||
|
▀▀▄▄▀
|
||||||
|
|
|
@ -28,7 +28,7 @@ class SimpleView(HTTPMethodView):
|
||||||
def delete(self, request):
|
def delete(self, request):
|
||||||
return text('I am delete method')
|
return text('I am delete method')
|
||||||
|
|
||||||
app.add_route(SimpleView(), '/')
|
app.add_route(SimpleView.as_view(), '/')
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -40,6 +40,19 @@ class NameView(HTTPMethodView):
|
||||||
def get(self, request, name):
|
def get(self, request, name):
|
||||||
return text('Hello {}'.format(name))
|
return text('Hello {}'.format(name))
|
||||||
|
|
||||||
app.add_route(NameView(), '/<name>')
|
app.add_route(NameView.as_view(), '/<name>')
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to add decorator for class, you could set decorators variable
|
||||||
|
|
||||||
|
```
|
||||||
|
class ViewWithDecorator(HTTPMethodView):
|
||||||
|
decorators = [some_decorator_here]
|
||||||
|
|
||||||
|
def get(self, request, name):
|
||||||
|
return text('Hello I have a decorator')
|
||||||
|
|
||||||
|
app.add_route(ViewWithDecorator.as_view(), '/url')
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
70
docs/custom_protocol.md
Normal file
70
docs/custom_protocol.md
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# Custom Protocol
|
||||||
|
|
||||||
|
You can change the behavior of protocol by using custom protocol.
|
||||||
|
If you want to use custom protocol, you should put subclass of [protocol class](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes) in the protocol keyword argument of `sanic.run()`. The constructor of custom protocol class gets following keyword arguments from Sanic.
|
||||||
|
|
||||||
|
* loop
|
||||||
|
`loop` is an asyncio compatible event loop.
|
||||||
|
|
||||||
|
* connections
|
||||||
|
`connections` is a `set object` to store protocol objects.
|
||||||
|
When Sanic receives `SIGINT` or `SIGTERM`, Sanic executes `protocol.close_if_idle()` for a `protocol objects` stored in connections.
|
||||||
|
|
||||||
|
* signal
|
||||||
|
`signal` is a `sanic.server.Signal object` with `stopped attribute`.
|
||||||
|
When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`.
|
||||||
|
|
||||||
|
* request_handler
|
||||||
|
`request_handler` is a coroutine that takes a `sanic.request.Request` object and a `response callback` as arguments.
|
||||||
|
|
||||||
|
* error_handler
|
||||||
|
`error_handler` is a `sanic.exceptions.Handler` object.
|
||||||
|
|
||||||
|
* request_timeout
|
||||||
|
`request_timeout` is seconds for timeout.
|
||||||
|
|
||||||
|
* request_max_size
|
||||||
|
`request_max_size` is bytes of max request size.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
By default protocol, an error occurs, if the handler does not return an `HTTPResponse object`.
|
||||||
|
In this example, By rewriting `write_response()`, if the handler returns `str`, it will be converted to an `HTTPResponse object`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.server import HttpProtocol
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomHttpProtocol(HttpProtocol):
|
||||||
|
|
||||||
|
def __init__(self, *, loop, request_handler, error_handler,
|
||||||
|
signal, connections, request_timeout, request_max_size):
|
||||||
|
super().__init__(
|
||||||
|
loop=loop, request_handler=request_handler,
|
||||||
|
error_handler=error_handler, signal=signal,
|
||||||
|
connections=connections, request_timeout=request_timeout,
|
||||||
|
request_max_size=request_max_size)
|
||||||
|
|
||||||
|
def write_response(self, response):
|
||||||
|
if isinstance(response, str):
|
||||||
|
response = text(response)
|
||||||
|
self.transport.write(
|
||||||
|
response.output(self.request.version)
|
||||||
|
)
|
||||||
|
self.transport.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def string(request):
|
||||||
|
return 'string'
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/1')
|
||||||
|
async def response(request):
|
||||||
|
return text('response')
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)
|
||||||
|
```
|
6
docs/extensions.md
Normal file
6
docs/extensions.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# Sanic Extensions
|
||||||
|
|
||||||
|
A list of Sanic extensions created by the community.
|
||||||
|
|
||||||
|
* [Sessions](https://github.com/subyraman/sanic_session) — Support for sessions. Allows using redis, memcache or an in memory store.
|
||||||
|
* [CORS](https://github.com/ashleysommer/sanic-cors) — A port of flask-cors.
|
|
@ -9,6 +9,7 @@ The following request variables are accessible as properties:
|
||||||
`request.args` (dict) - Query String variables. Use getlist to get multiple of the same name
|
`request.args` (dict) - Query String variables. Use getlist to get multiple of the same name
|
||||||
`request.form` (dict) - Posted form variables. Use getlist to get multiple of the same name
|
`request.form` (dict) - Posted form variables. Use getlist to get multiple of the same name
|
||||||
`request.body` (bytes) - Posted raw body. To get the raw data, regardless of content type
|
`request.body` (bytes) - Posted raw body. To get the raw data, regardless of content type
|
||||||
|
`request.ip` (str) - IP address of the requester
|
||||||
|
|
||||||
See request.py for more information
|
See request.py for more information
|
||||||
|
|
||||||
|
|
|
@ -33,12 +33,12 @@ async def handler1(request):
|
||||||
return text('OK')
|
return text('OK')
|
||||||
app.add_route(handler1, '/test')
|
app.add_route(handler1, '/test')
|
||||||
|
|
||||||
async def handler(request, name):
|
async def handler2(request, name):
|
||||||
return text('Folder - {}'.format(name))
|
return text('Folder - {}'.format(name))
|
||||||
app.add_route(handler, '/folder/<name>')
|
app.add_route(handler2, '/folder/<name>')
|
||||||
|
|
||||||
async def person_handler(request, name):
|
async def person_handler2(request, name):
|
||||||
return text('Person - {}'.format(name))
|
return text('Person - {}'.format(name))
|
||||||
app.add_route(handler, '/person/<name:[A-z]>')
|
app.add_route(person_handler2, '/person/<name:[A-z]>')
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
23
examples/override_logging.py
Normal file
23
examples/override_logging.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import text
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
|
||||||
|
logging_format += "%(module)s::%(funcName)s():l%(lineno)d: "
|
||||||
|
logging_format += "%(message)s"
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
format=logging_format,
|
||||||
|
level=logging.DEBUG
|
||||||
|
)
|
||||||
|
log = logging.getLogger()
|
||||||
|
|
||||||
|
# Set logger to override default basicConfig
|
||||||
|
sanic = Sanic()
|
||||||
|
@sanic.route("/")
|
||||||
|
def test(request):
|
||||||
|
log.info("received request; responding with 'hey'")
|
||||||
|
return text("hey")
|
||||||
|
|
||||||
|
sanic.run(host="0.0.0.0", port=8000)
|
65
examples/sanic_asyncpg_example.py
Normal file
65
examples/sanic_asyncpg_example.py
Normal 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)
|
|
@ -64,11 +64,11 @@ def query_string(request):
|
||||||
# Run Server
|
# Run Server
|
||||||
# ----------------------------------------------- #
|
# ----------------------------------------------- #
|
||||||
|
|
||||||
def after_start(loop):
|
def after_start(app, loop):
|
||||||
log.info("OH OH OH OH OHHHHHHHH")
|
log.info("OH OH OH OH OHHHHHHHH")
|
||||||
|
|
||||||
|
|
||||||
def before_stop(loop):
|
def before_stop(app, loop):
|
||||||
log.info("TRIED EVERYTHING")
|
log.info("TRIED EVERYTHING")
|
||||||
|
|
||||||
|
|
||||||
|
|
32
examples/vhosts.py
Normal file
32
examples/vhosts.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
from sanic.response import text
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.blueprints import Blueprint
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
# curl -H "Host: example.com" localhost:8000
|
||||||
|
# curl -H "Host: sub.example.com" localhost:8000
|
||||||
|
# curl -H "Host: bp.example.com" localhost:8000/question
|
||||||
|
# curl -H "Host: bp.example.com" localhost:8000/answer
|
||||||
|
|
||||||
|
app = Sanic()
|
||||||
|
bp = Blueprint("bp", host="bp.example.com")
|
||||||
|
|
||||||
|
@app.route('/', host="example.com")
|
||||||
|
async def hello(request):
|
||||||
|
return text("Answer")
|
||||||
|
@app.route('/', host="sub.example.com")
|
||||||
|
async def hello(request):
|
||||||
|
return text("42")
|
||||||
|
|
||||||
|
@bp.route("/question")
|
||||||
|
async def hello(request):
|
||||||
|
return text("What is the meaning of life?")
|
||||||
|
|
||||||
|
@bp.route("/answer")
|
||||||
|
async def hello(request):
|
||||||
|
return text("42")
|
||||||
|
|
||||||
|
app.register_blueprint(bp)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host="0.0.0.0", port=8000)
|
|
@ -14,3 +14,4 @@ tornado
|
||||||
aiofiles
|
aiofiles
|
||||||
sphinx
|
sphinx
|
||||||
recommonmark
|
recommonmark
|
||||||
|
beautifulsoup4
|
||||||
|
|
|
@ -2,4 +2,3 @@ httptools
|
||||||
ujson
|
ujson
|
||||||
uvloop
|
uvloop
|
||||||
aiofiles
|
aiofiles
|
||||||
multidict
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from .sanic import Sanic
|
from .sanic import Sanic
|
||||||
from .blueprints import Blueprint
|
from .blueprints import Blueprint
|
||||||
|
|
||||||
__version__ = '0.1.9'
|
__version__ = '0.2.0'
|
||||||
|
|
||||||
__all__ = ['Sanic', 'Blueprint']
|
__all__ = ['Sanic', 'Blueprint']
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -18,14 +18,17 @@ class BlueprintSetup:
|
||||||
#: blueprint.
|
#: blueprint.
|
||||||
self.url_prefix = url_prefix
|
self.url_prefix = url_prefix
|
||||||
|
|
||||||
def add_route(self, handler, uri, methods):
|
def add_route(self, handler, uri, methods, host=None):
|
||||||
"""
|
"""
|
||||||
A helper method to register a handler to the application url routes.
|
A helper method to register a handler to the application url routes.
|
||||||
"""
|
"""
|
||||||
if self.url_prefix:
|
if self.url_prefix:
|
||||||
uri = self.url_prefix + uri
|
uri = self.url_prefix + uri
|
||||||
|
|
||||||
self.app.route(uri=uri, methods=methods)(handler)
|
if host is None:
|
||||||
|
host = self.blueprint.host
|
||||||
|
|
||||||
|
self.app.route(uri=uri, methods=methods, host=host)(handler)
|
||||||
|
|
||||||
def add_exception(self, handler, *args, **kwargs):
|
def add_exception(self, handler, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -53,7 +56,7 @@ class BlueprintSetup:
|
||||||
|
|
||||||
|
|
||||||
class Blueprint:
|
class Blueprint:
|
||||||
def __init__(self, name, url_prefix=None):
|
def __init__(self, name, url_prefix=None, host=None):
|
||||||
"""
|
"""
|
||||||
Creates a new blueprint
|
Creates a new blueprint
|
||||||
:param name: Unique name of the blueprint
|
:param name: Unique name of the blueprint
|
||||||
|
@ -63,6 +66,7 @@ class Blueprint:
|
||||||
self.url_prefix = url_prefix
|
self.url_prefix = url_prefix
|
||||||
self.deferred_functions = []
|
self.deferred_functions = []
|
||||||
self.listeners = defaultdict(list)
|
self.listeners = defaultdict(list)
|
||||||
|
self.host = host
|
||||||
|
|
||||||
def record(self, func):
|
def record(self, func):
|
||||||
"""
|
"""
|
||||||
|
@ -83,18 +87,18 @@ class Blueprint:
|
||||||
for deferred in self.deferred_functions:
|
for deferred in self.deferred_functions:
|
||||||
deferred(state)
|
deferred(state)
|
||||||
|
|
||||||
def route(self, uri, methods=None):
|
def route(self, uri, methods=None, host=None):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
def decorator(handler):
|
def decorator(handler):
|
||||||
self.record(lambda s: s.add_route(handler, uri, methods))
|
self.record(lambda s: s.add_route(handler, uri, methods, host))
|
||||||
return handler
|
return handler
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def add_route(self, handler, uri, methods=None):
|
def add_route(self, handler, uri, methods=None, host=None):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
self.record(lambda s: s.add_route(handler, uri, methods))
|
self.record(lambda s: s.add_route(handler, uri, methods, host))
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
def listener(self, event):
|
def listener(self, event):
|
||||||
|
|
|
@ -1,5 +1,104 @@
|
||||||
from .response import text
|
from .response import text, html
|
||||||
from traceback import format_exc
|
from .log import log
|
||||||
|
from traceback import format_exc, extract_tb
|
||||||
|
import sys
|
||||||
|
|
||||||
|
TRACEBACK_STYLE = '''
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 20px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 code {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-line > * {
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-line {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-code {
|
||||||
|
font-size: 16px;
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-wrapper {
|
||||||
|
border: 1px solid #f3f3f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-header {
|
||||||
|
background-color: #f3f3f3;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-descriptor {
|
||||||
|
background-color: #e2eafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-descriptor {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
'''
|
||||||
|
|
||||||
|
TRACEBACK_WRAPPER_HTML = '''
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{style}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{exc_name}</h1>
|
||||||
|
<h3><code>{exc_value}</code></h3>
|
||||||
|
<div class="tb-wrapper">
|
||||||
|
<p class="tb-header">Traceback (most recent call last):</p>
|
||||||
|
{frame_html}
|
||||||
|
<p class="summary">
|
||||||
|
<b>{exc_name}: {exc_value}</b>
|
||||||
|
while handling uri <code>{uri}</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
'''
|
||||||
|
|
||||||
|
TRACEBACK_LINE_HTML = '''
|
||||||
|
<div class="frame-line">
|
||||||
|
<p class="frame-descriptor">
|
||||||
|
File {0.filename}, line <i>{0.lineno}</i>,
|
||||||
|
in <code><b>{0.name}</b></code>
|
||||||
|
</p>
|
||||||
|
<p class="frame-code"><code>{0.line}</code></p>
|
||||||
|
</div>
|
||||||
|
'''
|
||||||
|
|
||||||
|
INTERNAL_SERVER_ERROR_HTML = '''
|
||||||
|
<h1>Internal Server Error</h1>
|
||||||
|
<p>
|
||||||
|
The server encountered an internal error and cannot complete
|
||||||
|
your request.
|
||||||
|
</p>
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
class SanicException(Exception):
|
class SanicException(Exception):
|
||||||
|
@ -45,6 +144,21 @@ class Handler:
|
||||||
self.handlers = {}
|
self.handlers = {}
|
||||||
self.sanic = sanic
|
self.sanic = sanic
|
||||||
|
|
||||||
|
def _render_traceback_html(self, exception, request):
|
||||||
|
exc_type, exc_value, tb = sys.exc_info()
|
||||||
|
frames = extract_tb(tb)
|
||||||
|
|
||||||
|
frame_html = []
|
||||||
|
for frame in frames:
|
||||||
|
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
|
||||||
|
|
||||||
|
return TRACEBACK_WRAPPER_HTML.format(
|
||||||
|
style=TRACEBACK_STYLE,
|
||||||
|
exc_name=exc_type.__name__,
|
||||||
|
exc_value=exc_value,
|
||||||
|
frame_html=''.join(frame_html),
|
||||||
|
uri=request.url)
|
||||||
|
|
||||||
def add(self, exception, handler):
|
def add(self, exception, handler):
|
||||||
self.handlers[exception] = handler
|
self.handlers[exception] = handler
|
||||||
|
|
||||||
|
@ -57,18 +171,32 @@ class Handler:
|
||||||
:return: Response object
|
:return: Response object
|
||||||
"""
|
"""
|
||||||
handler = self.handlers.get(type(exception), self.default)
|
handler = self.handlers.get(type(exception), self.default)
|
||||||
|
try:
|
||||||
response = handler(request=request, exception=exception)
|
response = handler(request=request, exception=exception)
|
||||||
|
except:
|
||||||
|
if self.sanic.debug:
|
||||||
|
response_message = (
|
||||||
|
'Exception raised in exception handler "{}" '
|
||||||
|
'for uri: "{}"\n{}').format(
|
||||||
|
handler.__name__, request.url, format_exc())
|
||||||
|
log.error(response_message)
|
||||||
|
return text(response_message, 500)
|
||||||
|
else:
|
||||||
|
return text('An error occurred while handling an error', 500)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def default(self, request, exception):
|
def default(self, request, exception):
|
||||||
if issubclass(type(exception), SanicException):
|
if issubclass(type(exception), SanicException):
|
||||||
return text(
|
return text(
|
||||||
"Error: {}".format(exception),
|
'Error: {}'.format(exception),
|
||||||
status=getattr(exception, 'status_code', 500))
|
status=getattr(exception, 'status_code', 500))
|
||||||
elif self.sanic.debug:
|
elif self.sanic.debug:
|
||||||
return text(
|
html_output = self._render_traceback_html(exception, request)
|
||||||
"Error: {}\nException: {}".format(
|
|
||||||
exception, format_exc()), status=500)
|
response_message = (
|
||||||
|
'Exception occurred while handling uri: "{}"\n{}'.format(
|
||||||
|
request.url, format_exc()))
|
||||||
|
log.error(response_message)
|
||||||
|
return html(html_output, status=500)
|
||||||
else:
|
else:
|
||||||
return text(
|
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
|
||||||
"An error occurred while generating the request", status=500)
|
|
||||||
|
|
|
@ -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__)
|
||||||
|
|
|
@ -25,6 +25,9 @@ class RequestParameters(dict):
|
||||||
self.super = super()
|
self.super = super()
|
||||||
self.super.__init__(*args, **kwargs)
|
self.super.__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def __getitem__(self, name):
|
||||||
|
return self.get(name)
|
||||||
|
|
||||||
def get(self, name, default=None):
|
def get(self, name, default=None):
|
||||||
values = self.super.get(name)
|
values = self.super.get(name)
|
||||||
return values[0] if values else default
|
return values[0] if values else default
|
||||||
|
@ -38,18 +41,20 @@ class Request(dict):
|
||||||
Properties of an HTTP request such as URL, headers, etc.
|
Properties of an HTTP request such as URL, headers, etc.
|
||||||
"""
|
"""
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'url', 'headers', 'version', 'method', '_cookies',
|
'url', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||||
'query_string', 'body',
|
'query_string', 'body',
|
||||||
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||||
|
'_ip',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, url_bytes, headers, version, method):
|
def __init__(self, url_bytes, headers, version, method, transport):
|
||||||
# TODO: Content-Encoding detection
|
# TODO: Content-Encoding detection
|
||||||
url_parsed = parse_url(url_bytes)
|
url_parsed = parse_url(url_bytes)
|
||||||
self.url = url_parsed.path.decode('utf-8')
|
self.url = url_parsed.path.decode('utf-8')
|
||||||
self.headers = headers
|
self.headers = headers
|
||||||
self.version = version
|
self.version = version
|
||||||
self.method = method
|
self.method = method
|
||||||
|
self.transport = transport
|
||||||
self.query_string = None
|
self.query_string = None
|
||||||
if url_parsed.query:
|
if url_parsed.query:
|
||||||
self.query_string = url_parsed.query.decode('utf-8')
|
self.query_string = url_parsed.query.decode('utf-8')
|
||||||
|
@ -64,7 +69,7 @@ class Request(dict):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def json(self):
|
def json(self):
|
||||||
if not self.parsed_json:
|
if self.parsed_json is None:
|
||||||
try:
|
try:
|
||||||
self.parsed_json = json_loads(self.body)
|
self.parsed_json = json_loads(self.body)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -72,6 +77,17 @@ class Request(dict):
|
||||||
|
|
||||||
return self.parsed_json
|
return self.parsed_json
|
||||||
|
|
||||||
|
@property
|
||||||
|
def token(self):
|
||||||
|
"""
|
||||||
|
Attempts to return the auth header token.
|
||||||
|
:return: token related to request
|
||||||
|
"""
|
||||||
|
auth_header = self.headers.get('Authorization')
|
||||||
|
if auth_header is not None:
|
||||||
|
return auth_header.split()[1]
|
||||||
|
return auth_header
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self):
|
def form(self):
|
||||||
if self.parsed_form is None:
|
if self.parsed_form is None:
|
||||||
|
@ -125,6 +141,12 @@ class Request(dict):
|
||||||
self._cookies = {}
|
self._cookies = {}
|
||||||
return self._cookies
|
return self._cookies
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ip(self):
|
||||||
|
if not hasattr(self, '_ip'):
|
||||||
|
self._ip = self.transport.get_extra_info('peername')
|
||||||
|
return self._ip
|
||||||
|
|
||||||
|
|
||||||
File = namedtuple('File', ['type', 'body', 'name'])
|
File = namedtuple('File', ['type', 'body', 'name'])
|
||||||
|
|
||||||
|
|
|
@ -83,10 +83,10 @@ class HTTPResponse:
|
||||||
if body is not None:
|
if body is not None:
|
||||||
try:
|
try:
|
||||||
# Try to encode it regularly
|
# Try to encode it regularly
|
||||||
self.body = body.encode('utf-8')
|
self.body = body.encode()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Convert it to a str if you can't
|
# Convert it to a str if you can't
|
||||||
self.body = str(body).encode('utf-8')
|
self.body = str(body).encode()
|
||||||
else:
|
else:
|
||||||
self.body = body_bytes
|
self.body = body_bytes
|
||||||
|
|
||||||
|
@ -103,10 +103,14 @@ class HTTPResponse:
|
||||||
|
|
||||||
headers = b''
|
headers = b''
|
||||||
if self.headers:
|
if self.headers:
|
||||||
headers = b''.join(
|
for name, value in self.headers.items():
|
||||||
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
|
try:
|
||||||
for name, value in self.headers.items()
|
headers += (
|
||||||
)
|
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')))
|
||||||
|
except AttributeError:
|
||||||
|
headers += (
|
||||||
|
b'%b: %b\r\n' % (
|
||||||
|
str(name).encode(), str(value).encode('utf-8')))
|
||||||
|
|
||||||
# Try to pull from the common codes first
|
# Try to pull from the common codes first
|
||||||
# Speeds up response rate 6% over pulling from all
|
# Speeds up response rate 6% over pulling from all
|
||||||
|
@ -165,3 +169,26 @@ async def file(location, mime_type=None, headers=None):
|
||||||
headers=headers,
|
headers=headers,
|
||||||
content_type=mime_type,
|
content_type=mime_type,
|
||||||
body_bytes=out_stream)
|
body_bytes=out_stream)
|
||||||
|
|
||||||
|
|
||||||
|
def redirect(to, headers=None, status=302,
|
||||||
|
content_type="text/html; charset=utf-8"):
|
||||||
|
"""
|
||||||
|
Aborts execution and causes a 302 redirect (by default).
|
||||||
|
|
||||||
|
:param to: path or fully qualified URL to redirect to
|
||||||
|
:param headers: optional dict of headers to include in the new request
|
||||||
|
:param status: status code (int) of the new request, defaults to 302
|
||||||
|
:param content_type:
|
||||||
|
the content type (string) of the response
|
||||||
|
:returns: the redirecting Response
|
||||||
|
"""
|
||||||
|
headers = headers or {}
|
||||||
|
|
||||||
|
# According to RFC 7231, a relative URI is now permitted.
|
||||||
|
headers['Location'] = to
|
||||||
|
|
||||||
|
return HTTPResponse(
|
||||||
|
status=status,
|
||||||
|
headers=headers,
|
||||||
|
content_type=content_type)
|
||||||
|
|
|
@ -23,6 +23,10 @@ class RouteExists(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RouteDoesNotExist(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Router:
|
class Router:
|
||||||
"""
|
"""
|
||||||
Router supports basic routing with parameters and method checks
|
Router supports basic routing with parameters and method checks
|
||||||
|
@ -31,16 +35,16 @@ class Router:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...])
|
@sanic.route('/my/url/<my_param>', methods=['GET', 'POST', ...])
|
||||||
def my_route(request, my_parameter):
|
def my_route(request, my_param):
|
||||||
do stuff...
|
do stuff...
|
||||||
|
|
||||||
or
|
or
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@sanic.route('/my/url/<my_paramter>:type', methods['GET', 'POST', ...])
|
@sanic.route('/my/url/<my_param:my_type>', methods['GET', 'POST', ...])
|
||||||
def my_route_with_type(request, my_parameter):
|
def my_route_with_type(request, my_param: my_type):
|
||||||
do stuff...
|
do stuff...
|
||||||
|
|
||||||
Parameters will be passed as keyword arguments to the request handling
|
Parameters will be passed as keyword arguments to the request handling
|
||||||
|
@ -59,8 +63,9 @@ class Router:
|
||||||
self.routes_static = {}
|
self.routes_static = {}
|
||||||
self.routes_dynamic = defaultdict(list)
|
self.routes_dynamic = defaultdict(list)
|
||||||
self.routes_always_check = []
|
self.routes_always_check = []
|
||||||
|
self.hosts = None
|
||||||
|
|
||||||
def add(self, uri, methods, handler):
|
def add(self, uri, methods, handler, host=None):
|
||||||
"""
|
"""
|
||||||
Adds a handler to the route list
|
Adds a handler to the route list
|
||||||
|
|
||||||
|
@ -71,6 +76,17 @@ class Router:
|
||||||
When executed, it should provide a response object.
|
When executed, it should provide a response object.
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if host is not None:
|
||||||
|
# we want to track if there are any
|
||||||
|
# vhosts on the Router instance so that we can
|
||||||
|
# default to the behavior without vhosts
|
||||||
|
if self.hosts is None:
|
||||||
|
self.hosts = set(host)
|
||||||
|
else:
|
||||||
|
self.hosts.add(host)
|
||||||
|
uri = host + uri
|
||||||
|
|
||||||
if uri in self.routes_all:
|
if uri in self.routes_all:
|
||||||
raise RouteExists("Route already registered: {}".format(uri))
|
raise RouteExists("Route already registered: {}".format(uri))
|
||||||
|
|
||||||
|
@ -118,6 +134,25 @@ class Router:
|
||||||
else:
|
else:
|
||||||
self.routes_static[uri] = route
|
self.routes_static[uri] = route
|
||||||
|
|
||||||
|
def remove(self, uri, clean_cache=True, host=None):
|
||||||
|
if host is not None:
|
||||||
|
uri = host + uri
|
||||||
|
try:
|
||||||
|
route = self.routes_all.pop(uri)
|
||||||
|
except KeyError:
|
||||||
|
raise RouteDoesNotExist("Route was not registered: {}".format(uri))
|
||||||
|
|
||||||
|
if route in self.routes_always_check:
|
||||||
|
self.routes_always_check.remove(route)
|
||||||
|
elif url_hash(uri) in self.routes_dynamic \
|
||||||
|
and route in self.routes_dynamic[url_hash(uri)]:
|
||||||
|
self.routes_dynamic[url_hash(uri)].remove(route)
|
||||||
|
else:
|
||||||
|
self.routes_static.pop(uri)
|
||||||
|
|
||||||
|
if clean_cache:
|
||||||
|
self._get.cache_clear()
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""
|
"""
|
||||||
Gets a request handler based on the URL of the request, or raises an
|
Gets a request handler based on the URL of the request, or raises an
|
||||||
|
@ -126,10 +161,14 @@ class Router:
|
||||||
:param request: Request object
|
:param request: Request object
|
||||||
:return: handler, arguments, keyword arguments
|
:return: handler, arguments, keyword arguments
|
||||||
"""
|
"""
|
||||||
return self._get(request.url, request.method)
|
if self.hosts is None:
|
||||||
|
return self._get(request.url, request.method, '')
|
||||||
|
else:
|
||||||
|
return self._get(request.url, request.method,
|
||||||
|
request.headers.get("Host", ''))
|
||||||
|
|
||||||
@lru_cache(maxsize=Config.ROUTER_CACHE_SIZE)
|
@lru_cache(maxsize=Config.ROUTER_CACHE_SIZE)
|
||||||
def _get(self, url, method):
|
def _get(self, url, method, host):
|
||||||
"""
|
"""
|
||||||
Gets a request handler based on the URL of the request, or raises an
|
Gets a request handler based on the URL of the request, or raises an
|
||||||
error. Internal method for caching.
|
error. Internal method for caching.
|
||||||
|
@ -137,6 +176,7 @@ class Router:
|
||||||
:param method: Request method
|
:param method: Request method
|
||||||
:return: handler, arguments, keyword arguments
|
:return: handler, arguments, keyword arguments
|
||||||
"""
|
"""
|
||||||
|
url = host + url
|
||||||
# Check against known static routes
|
# Check against known static routes
|
||||||
route = self.routes_static.get(url)
|
route = self.routes_static.get(url)
|
||||||
if route:
|
if route:
|
||||||
|
|
|
@ -4,21 +4,24 @@ from functools import partial
|
||||||
from inspect import isawaitable, stack, getmodulename
|
from inspect import isawaitable, stack, getmodulename
|
||||||
from multiprocessing import Process, Event
|
from multiprocessing import Process, Event
|
||||||
from signal import signal, SIGTERM, SIGINT
|
from signal import signal, SIGTERM, SIGINT
|
||||||
from time import sleep
|
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
import logging
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .exceptions import Handler
|
from .exceptions import Handler
|
||||||
from .log import log, logging
|
from .log import log
|
||||||
from .response import HTTPResponse
|
from .response import HTTPResponse
|
||||||
from .router import Router
|
from .router import Router
|
||||||
from .server import serve
|
from .server import serve, HttpProtocol
|
||||||
from .static import register as static_register
|
from .static import register as static_register
|
||||||
from .exceptions import ServerError
|
from .exceptions import ServerError
|
||||||
|
from socket import socket, SOL_SOCKET, SO_REUSEADDR
|
||||||
|
from os import set_inheritable
|
||||||
|
|
||||||
|
|
||||||
class Sanic:
|
class Sanic:
|
||||||
def __init__(self, name=None, router=None, error_handler=None):
|
def __init__(self, name=None, router=None,
|
||||||
|
error_handler=None):
|
||||||
if name is None:
|
if name is None:
|
||||||
frame_records = stack()[1]
|
frame_records = stack()[1]
|
||||||
name = getmodulename(frame_records[1])
|
name = getmodulename(frame_records[1])
|
||||||
|
@ -32,6 +35,8 @@ class Sanic:
|
||||||
self._blueprint_order = []
|
self._blueprint_order = []
|
||||||
self.loop = None
|
self.loop = None
|
||||||
self.debug = None
|
self.debug = None
|
||||||
|
self.sock = None
|
||||||
|
self.processes = None
|
||||||
|
|
||||||
# Register alternative method names
|
# Register alternative method names
|
||||||
self.go_fast = self.run
|
self.go_fast = self.run
|
||||||
|
@ -41,7 +46,7 @@ class Sanic:
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
# Decorator
|
# Decorator
|
||||||
def route(self, uri, methods=None):
|
def route(self, uri, methods=None, host=None):
|
||||||
"""
|
"""
|
||||||
Decorates a function to be registered as a route
|
Decorates a function to be registered as a route
|
||||||
|
|
||||||
|
@ -56,12 +61,13 @@ class Sanic:
|
||||||
uri = '/' + uri
|
uri = '/' + uri
|
||||||
|
|
||||||
def response(handler):
|
def response(handler):
|
||||||
self.router.add(uri=uri, methods=methods, handler=handler)
|
self.router.add(uri=uri, methods=methods, handler=handler,
|
||||||
|
host=host)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def add_route(self, handler, uri, methods=None):
|
def add_route(self, handler, uri, methods=None, host=None):
|
||||||
"""
|
"""
|
||||||
A helper method to register class instance or
|
A helper method to register class instance or
|
||||||
functions as a handler to the application url
|
functions as a handler to the application url
|
||||||
|
@ -72,9 +78,12 @@ class Sanic:
|
||||||
:param methods: list or tuple of methods allowed
|
:param methods: list or tuple of methods allowed
|
||||||
:return: function or class instance
|
:return: function or class instance
|
||||||
"""
|
"""
|
||||||
self.route(uri=uri, methods=methods)(handler)
|
self.route(uri=uri, methods=methods, host=host)(handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
|
def remove_route(self, uri, clean_cache=True, host=None):
|
||||||
|
self.router.remove(uri, clean_cache, host)
|
||||||
|
|
||||||
# Decorator
|
# Decorator
|
||||||
def exception(self, *exceptions):
|
def exception(self, *exceptions):
|
||||||
"""
|
"""
|
||||||
|
@ -144,7 +153,8 @@ class Sanic:
|
||||||
def register_blueprint(self, *args, **kwargs):
|
def register_blueprint(self, *args, **kwargs):
|
||||||
# TODO: deprecate 1.0
|
# TODO: deprecate 1.0
|
||||||
log.warning("Use of register_blueprint will be deprecated in "
|
log.warning("Use of register_blueprint will be deprecated in "
|
||||||
"version 1.0. Please use the blueprint method instead")
|
"version 1.0. Please use the blueprint method instead",
|
||||||
|
DeprecationWarning)
|
||||||
return self.blueprint(*args, **kwargs)
|
return self.blueprint(*args, **kwargs)
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
@ -237,7 +247,8 @@ class Sanic:
|
||||||
|
|
||||||
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
|
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
|
||||||
after_start=None, before_stop=None, after_stop=None, sock=None,
|
after_start=None, before_stop=None, after_stop=None, sock=None,
|
||||||
workers=1, loop=None):
|
workers=1, loop=None, protocol=HttpProtocol, backlog=100,
|
||||||
|
stop_event=None, logger=None):
|
||||||
"""
|
"""
|
||||||
Runs the HTTP Server and listens until keyboard interrupt or term
|
Runs the HTTP Server and listens until keyboard interrupt or term
|
||||||
signal. On termination, drains connections before closing.
|
signal. On termination, drains connections before closing.
|
||||||
|
@ -245,25 +256,32 @@ class Sanic:
|
||||||
:param host: Address to host on
|
:param host: Address to host on
|
||||||
:param port: Port to host on
|
:param port: Port to host on
|
||||||
:param debug: Enables debug output (slows server)
|
:param debug: Enables debug output (slows server)
|
||||||
:param before_start: Function to be executed before the server starts
|
:param before_start: Functions to be executed before the server starts
|
||||||
accepting connections
|
accepting connections
|
||||||
:param after_start: Function to be executed after the server starts
|
:param after_start: Functions to be executed after the server starts
|
||||||
accepting connections
|
accepting connections
|
||||||
:param before_stop: Function to be executed when a stop signal is
|
:param before_stop: Functions to be executed when a stop signal is
|
||||||
received before it is respected
|
received before it is respected
|
||||||
:param after_stop: Function to be executed when all requests are
|
:param after_stop: Functions to be executed when all requests are
|
||||||
complete
|
complete
|
||||||
|
|
||||||
:param sock: Socket for the server to accept connections from
|
:param sock: Socket for the server to accept connections from
|
||||||
:param workers: Number of processes
|
:param workers: Number of processes
|
||||||
received before it is respected
|
received before it is respected
|
||||||
:param loop: asyncio compatible event loop
|
:param loop: asyncio compatible event loop
|
||||||
|
:param protocol: Subclass of asyncio protocol class
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s: %(levelname)s: %(message)s"
|
||||||
|
)
|
||||||
self.error_handler.debug = True
|
self.error_handler.debug = True
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
|
|
||||||
server_settings = {
|
server_settings = {
|
||||||
|
'protocol': protocol,
|
||||||
'host': host,
|
'host': host,
|
||||||
'port': port,
|
'port': port,
|
||||||
'sock': sock,
|
'sock': sock,
|
||||||
|
@ -272,7 +290,8 @@ class Sanic:
|
||||||
'error_handler': self.error_handler,
|
'error_handler': self.error_handler,
|
||||||
'request_timeout': self.config.REQUEST_TIMEOUT,
|
'request_timeout': self.config.REQUEST_TIMEOUT,
|
||||||
'request_max_size': self.config.REQUEST_MAX_SIZE,
|
'request_max_size': self.config.REQUEST_MAX_SIZE,
|
||||||
'loop': loop
|
'loop': loop,
|
||||||
|
'backlog': backlog
|
||||||
}
|
}
|
||||||
|
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
|
@ -289,7 +308,7 @@ class Sanic:
|
||||||
for blueprint in self.blueprints.values():
|
for blueprint in self.blueprints.values():
|
||||||
listeners += blueprint.listeners[event_name]
|
listeners += blueprint.listeners[event_name]
|
||||||
if args:
|
if args:
|
||||||
if type(args) is not list:
|
if callable(args):
|
||||||
args = [args]
|
args = [args]
|
||||||
listeners += args
|
listeners += args
|
||||||
if reverse:
|
if reverse:
|
||||||
|
@ -311,7 +330,7 @@ class Sanic:
|
||||||
else:
|
else:
|
||||||
log.info('Spinning up {} workers...'.format(workers))
|
log.info('Spinning up {} workers...'.format(workers))
|
||||||
|
|
||||||
self.serve_multiple(server_settings, workers)
|
self.serve_multiple(server_settings, workers, stop_event)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(
|
log.exception(
|
||||||
|
@ -323,10 +342,13 @@ class Sanic:
|
||||||
"""
|
"""
|
||||||
This kills the Sanic
|
This kills the Sanic
|
||||||
"""
|
"""
|
||||||
|
if self.processes is not None:
|
||||||
|
for process in self.processes:
|
||||||
|
process.terminate()
|
||||||
|
self.sock.close()
|
||||||
get_event_loop().stop()
|
get_event_loop().stop()
|
||||||
|
|
||||||
@staticmethod
|
def serve_multiple(self, server_settings, workers, stop_event=None):
|
||||||
def serve_multiple(server_settings, workers, stop_event=None):
|
|
||||||
"""
|
"""
|
||||||
Starts multiple server processes simultaneously. Stops on interrupt
|
Starts multiple server processes simultaneously. Stops on interrupt
|
||||||
and terminate signals, and drains connections when complete.
|
and terminate signals, and drains connections when complete.
|
||||||
|
@ -336,29 +358,37 @@ class Sanic:
|
||||||
:param stop_event: if provided, is used as a stop signal
|
:param stop_event: if provided, is used as a stop signal
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
# In case this is called directly, we configure logging here too.
|
||||||
|
# This won't interfere with the same call from run()
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s: %(levelname)s: %(message)s"
|
||||||
|
)
|
||||||
server_settings['reuse_port'] = True
|
server_settings['reuse_port'] = True
|
||||||
|
|
||||||
# Create a stop event to be triggered by a signal
|
# Create a stop event to be triggered by a signal
|
||||||
if not stop_event:
|
if stop_event is None:
|
||||||
stop_event = Event()
|
stop_event = Event()
|
||||||
signal(SIGINT, lambda s, f: stop_event.set())
|
signal(SIGINT, lambda s, f: stop_event.set())
|
||||||
signal(SIGTERM, lambda s, f: stop_event.set())
|
signal(SIGTERM, lambda s, f: stop_event.set())
|
||||||
|
|
||||||
processes = []
|
self.sock = socket()
|
||||||
|
self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||||
|
self.sock.bind((server_settings['host'], server_settings['port']))
|
||||||
|
set_inheritable(self.sock.fileno(), True)
|
||||||
|
server_settings['sock'] = self.sock
|
||||||
|
server_settings['host'] = None
|
||||||
|
server_settings['port'] = None
|
||||||
|
|
||||||
|
self.processes = []
|
||||||
for _ in range(workers):
|
for _ in range(workers):
|
||||||
process = Process(target=serve, kwargs=server_settings)
|
process = Process(target=serve, kwargs=server_settings)
|
||||||
|
process.daemon = True
|
||||||
process.start()
|
process.start()
|
||||||
processes.append(process)
|
self.processes.append(process)
|
||||||
|
|
||||||
# Infinitely wait for the stop event
|
for process in self.processes:
|
||||||
try:
|
|
||||||
while not stop_event.is_set():
|
|
||||||
sleep(0.3)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
log.info('Spinning down workers...')
|
|
||||||
for process in processes:
|
|
||||||
process.terminate()
|
|
||||||
for process in processes:
|
|
||||||
process.join()
|
process.join()
|
||||||
|
|
||||||
|
# the above processes will block this until they're stopped
|
||||||
|
self.stop()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import traceback
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from multidict import CIMultiDict
|
|
||||||
from signal import SIGINT, SIGTERM
|
from signal import SIGINT, SIGTERM
|
||||||
from time import time
|
from time import time
|
||||||
from httptools import HttpRequestParser
|
from httptools import HttpRequestParser
|
||||||
|
@ -18,11 +18,30 @@ from .request import Request
|
||||||
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
|
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
|
||||||
|
|
||||||
|
|
||||||
|
current_time = None
|
||||||
|
|
||||||
|
|
||||||
class Signal:
|
class Signal:
|
||||||
stopped = False
|
stopped = False
|
||||||
|
|
||||||
|
|
||||||
current_time = None
|
class CIDict(dict):
|
||||||
|
"""
|
||||||
|
Case Insensitive dict where all keys are converted to lowercase
|
||||||
|
This does not maintain the inputted case when calling items() or keys()
|
||||||
|
in favor of speed, since headers are case insensitive
|
||||||
|
"""
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return super().get(key.casefold(), default)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return super().__getitem__(key.casefold())
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
return super().__setitem__(key.casefold(), value)
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return super().__contains__(key.casefold())
|
||||||
|
|
||||||
|
|
||||||
class HttpProtocol(asyncio.Protocol):
|
class HttpProtocol(asyncio.Protocol):
|
||||||
|
@ -118,18 +137,15 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
exception = PayloadTooLarge('Payload Too Large')
|
exception = PayloadTooLarge('Payload Too Large')
|
||||||
self.write_error(exception)
|
self.write_error(exception)
|
||||||
|
|
||||||
self.headers.append((name.decode(), value.decode('utf-8')))
|
self.headers.append((name.decode().casefold(), value.decode()))
|
||||||
|
|
||||||
def on_headers_complete(self):
|
def on_headers_complete(self):
|
||||||
remote_addr = self.transport.get_extra_info('peername')
|
|
||||||
if remote_addr:
|
|
||||||
self.headers.append(('Remote-Addr', '%s:%s' % remote_addr))
|
|
||||||
|
|
||||||
self.request = Request(
|
self.request = Request(
|
||||||
url_bytes=self.url,
|
url_bytes=self.url,
|
||||||
headers=CIMultiDict(self.headers),
|
headers=CIDict(self.headers),
|
||||||
version=self.parser.get_http_version(),
|
version=self.parser.get_http_version(),
|
||||||
method=self.parser.get_method().decode()
|
method=self.parser.get_method().decode(),
|
||||||
|
transport=self.transport
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_body(self, body):
|
def on_body(self, body):
|
||||||
|
@ -174,6 +190,12 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
"Writing error failed, connection closed {}".format(e))
|
"Writing error failed, connection closed {}".format(e))
|
||||||
|
|
||||||
def bail_out(self, message):
|
def bail_out(self, message):
|
||||||
|
if self.transport.is_closing():
|
||||||
|
log.error(
|
||||||
|
"Connection closed before error was sent to user @ {}".format(
|
||||||
|
self.transport.get_extra_info('peername')))
|
||||||
|
log.debug('Error experienced:\n{}'.format(traceback.format_exc()))
|
||||||
|
else:
|
||||||
exception = ServerError(message)
|
exception = ServerError(message)
|
||||||
self.write_error(exception)
|
self.write_error(exception)
|
||||||
log.error(message)
|
log.error(message)
|
||||||
|
@ -225,26 +247,33 @@ def trigger_events(events, loop):
|
||||||
|
|
||||||
|
|
||||||
def serve(host, port, request_handler, error_handler, before_start=None,
|
def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
after_start=None, before_stop=None, after_stop=None,
|
after_start=None, before_stop=None, after_stop=None, debug=False,
|
||||||
debug=False, request_timeout=60, sock=None,
|
request_timeout=60, sock=None, request_max_size=None,
|
||||||
request_max_size=None, reuse_port=False, loop=None):
|
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100):
|
||||||
"""
|
"""
|
||||||
Starts asynchronous HTTP Server on an individual process.
|
Starts asynchronous HTTP Server on an individual process.
|
||||||
|
|
||||||
:param host: Address to host on
|
:param host: Address to host on
|
||||||
:param port: Port to host on
|
:param port: Port to host on
|
||||||
:param request_handler: Sanic request handler with middleware
|
:param request_handler: Sanic request handler with middleware
|
||||||
|
:param error_handler: Sanic error handler with middleware
|
||||||
|
:param before_start: Function to be executed before the server starts
|
||||||
|
listening. Takes single argument `loop`
|
||||||
:param after_start: Function to be executed after the server starts
|
:param after_start: Function to be executed after the server starts
|
||||||
listening. Takes single argument `loop`
|
listening. Takes single argument `loop`
|
||||||
:param before_stop: Function to be executed when a stop signal is
|
:param before_stop: Function to be executed when a stop signal is
|
||||||
received before it is respected. Takes single
|
received before it is respected. Takes single
|
||||||
argumenet `loop`
|
argument `loop`
|
||||||
|
:param after_stop: Function to be executed when a stop signal is
|
||||||
|
received after it is respected. Takes single
|
||||||
|
argument `loop`
|
||||||
:param debug: Enables debug output (slows server)
|
:param debug: Enables debug output (slows server)
|
||||||
:param request_timeout: time in seconds
|
:param request_timeout: time in seconds
|
||||||
:param sock: Socket for the server to accept connections from
|
:param sock: Socket for the server to accept connections from
|
||||||
:param request_max_size: size in bytes, `None` for no limit
|
:param request_max_size: size in bytes, `None` for no limit
|
||||||
:param reuse_port: `True` for multiple workers
|
:param reuse_port: `True` for multiple workers
|
||||||
:param loop: asyncio compatible event loop
|
:param loop: asyncio compatible event loop
|
||||||
|
:param protocol: Subclass of asyncio protocol class
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
loop = loop or async_loop.new_event_loop()
|
loop = loop or async_loop.new_event_loop()
|
||||||
|
@ -258,7 +287,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
connections = set()
|
connections = set()
|
||||||
signal = Signal()
|
signal = Signal()
|
||||||
server = partial(
|
server = partial(
|
||||||
HttpProtocol,
|
protocol,
|
||||||
loop=loop,
|
loop=loop,
|
||||||
connections=connections,
|
connections=connections,
|
||||||
signal=signal,
|
signal=signal,
|
||||||
|
@ -273,7 +302,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
reuse_port=reuse_port,
|
reuse_port=reuse_port,
|
||||||
sock=sock
|
sock=sock,
|
||||||
|
backlog=backlog
|
||||||
)
|
)
|
||||||
|
|
||||||
# Instead of pulling time at the end of every request,
|
# Instead of pulling time at the end of every request,
|
||||||
|
|
|
@ -16,15 +16,15 @@ async def local_request(method, uri, cookies=None, *args, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||||
loop=None, debug=False, *request_args,
|
loop=None, debug=False, server_kwargs={},
|
||||||
**request_kwargs):
|
*request_args, **request_kwargs):
|
||||||
results = []
|
results = []
|
||||||
exceptions = []
|
exceptions = []
|
||||||
|
|
||||||
if gather_request:
|
if gather_request:
|
||||||
@app.middleware
|
|
||||||
def _collect_request(request):
|
def _collect_request(request):
|
||||||
results.append(request)
|
results.append(request)
|
||||||
|
app.request_middleware.appendleft(_collect_request)
|
||||||
|
|
||||||
async def _collect_response(sanic, loop):
|
async def _collect_response(sanic, loop):
|
||||||
try:
|
try:
|
||||||
|
@ -35,8 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||||
exceptions.append(e)
|
exceptions.append(e)
|
||||||
app.stop()
|
app.stop()
|
||||||
|
|
||||||
app.run(host=HOST, debug=debug, port=42101,
|
app.run(host=HOST, debug=debug, port=PORT,
|
||||||
after_start=_collect_response, loop=loop)
|
after_start=_collect_response, loop=loop, **server_kwargs)
|
||||||
|
|
||||||
if exceptions:
|
if exceptions:
|
||||||
raise ValueError("Exception during request: {}".format(exceptions))
|
raise ValueError("Exception during request: {}".format(exceptions))
|
||||||
|
|
|
@ -10,7 +10,7 @@ class HTTPMethodView:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class DummyView(View):
|
class DummyView(HTTPMethodView):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
return text('I am get method')
|
return text('I am get method')
|
||||||
def put(self, request, *args, **kwargs):
|
def put(self, request, *args, **kwargs):
|
||||||
|
@ -25,19 +25,43 @@ class HTTPMethodView:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class DummyView(View):
|
class DummyView(HTTPMethodView):
|
||||||
def get(self, request, my_param_here, *args, **kwargs):
|
def get(self, request, my_param_here, *args, **kwargs):
|
||||||
return text('I am get method with %s' % my_param_here)
|
return text('I am get method with %s' % my_param_here)
|
||||||
|
|
||||||
To add the view into the routing you could use
|
To add the view into the routing you could use
|
||||||
1) app.add_route(DummyView(), '/')
|
1) app.add_route(DummyView.as_view(), '/')
|
||||||
2) app.route('/')(DummyView())
|
2) app.route('/')(DummyView.as_view())
|
||||||
|
|
||||||
|
To add any decorator you could set it into decorators variable
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __call__(self, request, *args, **kwargs):
|
decorators = []
|
||||||
|
|
||||||
|
def dispatch_request(self, request, *args, **kwargs):
|
||||||
handler = getattr(self, request.method.lower(), None)
|
handler = getattr(self, request.method.lower(), None)
|
||||||
if handler:
|
if handler:
|
||||||
return handler(request, *args, **kwargs)
|
return handler(request, *args, **kwargs)
|
||||||
raise InvalidUsage(
|
raise InvalidUsage(
|
||||||
'Method {} not allowed for URL {}'.format(
|
'Method {} not allowed for URL {}'.format(
|
||||||
request.method, request.url), status_code=405)
|
request.method, request.url), status_code=405)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def as_view(cls, *class_args, **class_kwargs):
|
||||||
|
""" Converts the class into an actual view function that can be used
|
||||||
|
with the routing system.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def view(*args, **kwargs):
|
||||||
|
self = view.view_class(*class_args, **class_kwargs)
|
||||||
|
return self.dispatch_request(*args, **kwargs)
|
||||||
|
|
||||||
|
if cls.decorators:
|
||||||
|
view.__module__ = cls.__module__
|
||||||
|
for decorator in cls.decorators:
|
||||||
|
view = decorator(view)
|
||||||
|
|
||||||
|
view.view_class = cls
|
||||||
|
view.__doc__ = cls.__doc__
|
||||||
|
view.__module__ = cls.__module__
|
||||||
|
return view
|
||||||
|
|
|
@ -59,6 +59,71 @@ def test_several_bp_with_url_prefix():
|
||||||
request, response = sanic_endpoint_test(app, uri='/test2/')
|
request, response = sanic_endpoint_test(app, uri='/test2/')
|
||||||
assert response.text == 'Hello2'
|
assert response.text == 'Hello2'
|
||||||
|
|
||||||
|
def test_bp_with_host():
|
||||||
|
app = Sanic('test_bp_host')
|
||||||
|
bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com")
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
def handler(request):
|
||||||
|
return text('Hello')
|
||||||
|
|
||||||
|
@bp.route('/', host="sub.example.com")
|
||||||
|
def handler(request):
|
||||||
|
return text('Hello subdomain!')
|
||||||
|
|
||||||
|
app.blueprint(bp)
|
||||||
|
headers = {"Host": "example.com"}
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test1/',
|
||||||
|
headers=headers)
|
||||||
|
assert response.text == 'Hello'
|
||||||
|
|
||||||
|
headers = {"Host": "sub.example.com"}
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test1/',
|
||||||
|
headers=headers)
|
||||||
|
|
||||||
|
assert response.text == 'Hello subdomain!'
|
||||||
|
|
||||||
|
|
||||||
|
def test_several_bp_with_host():
|
||||||
|
app = Sanic('test_text')
|
||||||
|
bp = Blueprint('test_text',
|
||||||
|
url_prefix='/test',
|
||||||
|
host="example.com")
|
||||||
|
bp2 = Blueprint('test_text2',
|
||||||
|
url_prefix='/test',
|
||||||
|
host="sub.example.com")
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
def handler(request):
|
||||||
|
return text('Hello')
|
||||||
|
|
||||||
|
@bp2.route('/')
|
||||||
|
def handler2(request):
|
||||||
|
return text('Hello2')
|
||||||
|
|
||||||
|
@bp2.route('/other/')
|
||||||
|
def handler2(request):
|
||||||
|
return text('Hello3')
|
||||||
|
|
||||||
|
|
||||||
|
app.blueprint(bp)
|
||||||
|
app.blueprint(bp2)
|
||||||
|
|
||||||
|
assert bp.host == "example.com"
|
||||||
|
headers = {"Host": "example.com"}
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test/',
|
||||||
|
headers=headers)
|
||||||
|
assert response.text == 'Hello'
|
||||||
|
|
||||||
|
assert bp2.host == "sub.example.com"
|
||||||
|
headers = {"Host": "sub.example.com"}
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test/',
|
||||||
|
headers=headers)
|
||||||
|
|
||||||
|
assert response.text == 'Hello2'
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test/other/',
|
||||||
|
headers=headers)
|
||||||
|
assert response.text == 'Hello3'
|
||||||
|
|
||||||
def test_bp_middleware():
|
def test_bp_middleware():
|
||||||
app = Sanic('test_middleware')
|
app = Sanic('test_middleware')
|
||||||
|
|
32
tests/test_custom_protocol.py
Normal file
32
tests/test_custom_protocol.py
Normal 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'
|
|
@ -1,51 +1,93 @@
|
||||||
|
import pytest
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
||||||
from sanic.utils import sanic_endpoint_test
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
|
||||||
# GET
|
|
||||||
# ------------------------------------------------------------ #
|
|
||||||
|
|
||||||
exception_app = Sanic('test_exceptions')
|
class SanicExceptionTestException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@exception_app.route('/')
|
@pytest.fixture(scope='module')
|
||||||
def handler(request):
|
def exception_app():
|
||||||
|
app = Sanic('test_exceptions')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def handler(request):
|
||||||
return text('OK')
|
return text('OK')
|
||||||
|
|
||||||
|
@app.route('/error')
|
||||||
@exception_app.route('/error')
|
def handler_error(request):
|
||||||
def handler_error(request):
|
|
||||||
raise ServerError("OK")
|
raise ServerError("OK")
|
||||||
|
|
||||||
|
@app.route('/404')
|
||||||
@exception_app.route('/404')
|
def handler_404(request):
|
||||||
def handler_404(request):
|
|
||||||
raise NotFound("OK")
|
raise NotFound("OK")
|
||||||
|
|
||||||
|
@app.route('/invalid')
|
||||||
@exception_app.route('/invalid')
|
def handler_invalid(request):
|
||||||
def handler_invalid(request):
|
|
||||||
raise InvalidUsage("OK")
|
raise InvalidUsage("OK")
|
||||||
|
|
||||||
|
@app.route('/divide_by_zero')
|
||||||
|
def handle_unhandled_exception(request):
|
||||||
|
1 / 0
|
||||||
|
|
||||||
def test_no_exception():
|
@app.route('/error_in_error_handler_handler')
|
||||||
|
def custom_error_handler(request):
|
||||||
|
raise SanicExceptionTestException('Dummy message!')
|
||||||
|
|
||||||
|
@app.exception(SanicExceptionTestException)
|
||||||
|
def error_in_error_handler_handler(request, exception):
|
||||||
|
1 / 0
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_exception(exception_app):
|
||||||
|
"""Test that a route works without an exception"""
|
||||||
request, response = sanic_endpoint_test(exception_app)
|
request, response = sanic_endpoint_test(exception_app)
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == 'OK'
|
assert response.text == 'OK'
|
||||||
|
|
||||||
|
|
||||||
def test_server_error_exception():
|
def test_server_error_exception(exception_app):
|
||||||
|
"""Test the built-in ServerError exception works"""
|
||||||
request, response = sanic_endpoint_test(exception_app, uri='/error')
|
request, response = sanic_endpoint_test(exception_app, uri='/error')
|
||||||
assert response.status == 500
|
assert response.status == 500
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_usage_exception():
|
def test_invalid_usage_exception(exception_app):
|
||||||
|
"""Test the built-in InvalidUsage exception works"""
|
||||||
request, response = sanic_endpoint_test(exception_app, uri='/invalid')
|
request, response = sanic_endpoint_test(exception_app, uri='/invalid')
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
|
||||||
|
|
||||||
def test_not_found_exception():
|
def test_not_found_exception(exception_app):
|
||||||
|
"""Test the built-in NotFound exception works"""
|
||||||
request, response = sanic_endpoint_test(exception_app, uri='/404')
|
request, response = sanic_endpoint_test(exception_app, uri='/404')
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_handled_unhandled_exception(exception_app):
|
||||||
|
"""Test that an exception not built into sanic is handled"""
|
||||||
|
request, response = sanic_endpoint_test(
|
||||||
|
exception_app, uri='/divide_by_zero')
|
||||||
|
assert response.status == 500
|
||||||
|
soup = BeautifulSoup(response.body, 'html.parser')
|
||||||
|
assert soup.h1.text == 'Internal Server Error'
|
||||||
|
|
||||||
|
message = " ".join(soup.p.text.split())
|
||||||
|
assert message == (
|
||||||
|
"The server encountered an internal error and "
|
||||||
|
"cannot complete your request.")
|
||||||
|
|
||||||
|
|
||||||
|
def test_exception_in_exception_handler(exception_app):
|
||||||
|
"""Test that an exception thrown in an error handler is handled"""
|
||||||
|
request, response = sanic_endpoint_test(
|
||||||
|
exception_app, uri='/error_in_error_handler_handler')
|
||||||
|
assert response.status == 500
|
||||||
|
assert response.body == b'An error occurred while handling an error'
|
||||||
|
|
|
@ -2,6 +2,7 @@ from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
||||||
from sanic.utils import sanic_endpoint_test
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
exception_handler_app = Sanic('test_exception_handler')
|
exception_handler_app = Sanic('test_exception_handler')
|
||||||
|
|
||||||
|
@ -21,6 +22,12 @@ def handler_3(request):
|
||||||
raise NotFound("OK")
|
raise NotFound("OK")
|
||||||
|
|
||||||
|
|
||||||
|
@exception_handler_app.route('/4')
|
||||||
|
def handler_4(request):
|
||||||
|
foo = bar
|
||||||
|
return text(foo)
|
||||||
|
|
||||||
|
|
||||||
@exception_handler_app.exception(NotFound, ServerError)
|
@exception_handler_app.exception(NotFound, ServerError)
|
||||||
def handler_exception(request, exception):
|
def handler_exception(request, exception):
|
||||||
return text("OK")
|
return text("OK")
|
||||||
|
@ -47,3 +54,20 @@ def test_text_exception__handler():
|
||||||
exception_handler_app, uri='/random')
|
exception_handler_app, uri='/random')
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == 'OK'
|
assert response.text == 'OK'
|
||||||
|
|
||||||
|
|
||||||
|
def test_html_traceback_output_in_debug_mode():
|
||||||
|
request, response = sanic_endpoint_test(
|
||||||
|
exception_handler_app, uri='/4', debug=True)
|
||||||
|
assert response.status == 500
|
||||||
|
soup = BeautifulSoup(response.body, 'html.parser')
|
||||||
|
html = str(soup)
|
||||||
|
|
||||||
|
assert 'response = handler(request, *args, **kwargs)' in html
|
||||||
|
assert 'handler_4' in html
|
||||||
|
assert 'foo = bar' in html
|
||||||
|
|
||||||
|
summary_text = " ".join(soup.select('.summary')[0].text.split())
|
||||||
|
assert (
|
||||||
|
"NameError: name 'bar' "
|
||||||
|
"is not defined while handling uri /4") == summary_text
|
||||||
|
|
33
tests/test_logging.py
Normal file
33
tests/test_logging.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import asyncio
|
||||||
|
from sanic.response import text
|
||||||
|
from sanic import Sanic
|
||||||
|
from io import StringIO
|
||||||
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging_format = '''module: %(module)s; \
|
||||||
|
function: %(funcName)s(); \
|
||||||
|
message: %(message)s'''
|
||||||
|
|
||||||
|
def test_log():
|
||||||
|
log_stream = StringIO()
|
||||||
|
for handler in logging.root.handlers[:]:
|
||||||
|
logging.root.removeHandler(handler)
|
||||||
|
logging.basicConfig(
|
||||||
|
format=logging_format,
|
||||||
|
level=logging.DEBUG,
|
||||||
|
stream=log_stream
|
||||||
|
)
|
||||||
|
log = logging.getLogger()
|
||||||
|
app = Sanic('test_logging')
|
||||||
|
@app.route('/')
|
||||||
|
def handler(request):
|
||||||
|
log.info('hello world')
|
||||||
|
return text('hello')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
log_text = log_stream.getvalue().strip().split('\n')[-3]
|
||||||
|
assert log_text == "module: test_logging; function: handler(); message: hello world"
|
||||||
|
|
||||||
|
if __name__ =="__main__":
|
||||||
|
test_log()
|
|
@ -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
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
from json import loads as json_loads, dumps as json_dumps
|
from json import loads as json_loads, dumps as json_dumps
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import json, text
|
from sanic.response import json, text, redirect
|
||||||
from sanic.utils import sanic_endpoint_test
|
from sanic.utils import sanic_endpoint_test
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
# GET
|
# GET
|
||||||
|
@ -33,6 +34,31 @@ def test_text():
|
||||||
assert response.text == 'Hello'
|
assert response.text == 'Hello'
|
||||||
|
|
||||||
|
|
||||||
|
def test_headers():
|
||||||
|
app = Sanic('test_text')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
headers = {"spam": "great"}
|
||||||
|
return text('Hello', headers=headers)
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
assert response.headers.get('spam') == 'great'
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_str_headers():
|
||||||
|
app = Sanic('test_text')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
headers = {"answer": 42}
|
||||||
|
return text('Hello', headers=headers)
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
assert response.headers.get('answer') == '42'
|
||||||
|
|
||||||
def test_invalid_response():
|
def test_invalid_response():
|
||||||
app = Sanic('test_invalid_response')
|
app = Sanic('test_invalid_response')
|
||||||
|
|
||||||
|
@ -92,6 +118,24 @@ def test_query_string():
|
||||||
assert request.args.get('test2') == 'false'
|
assert request.args.get('test2') == 'false'
|
||||||
|
|
||||||
|
|
||||||
|
def test_token():
|
||||||
|
app = Sanic('test_post_token')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
# uuid4 generated token.
|
||||||
|
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
|
||||||
|
headers = {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'Authorization': 'Token {}'.format(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, headers=headers)
|
||||||
|
|
||||||
|
assert request.token == token
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
# POST
|
# POST
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
|
@ -145,3 +189,73 @@ def test_post_form_multipart_form_data():
|
||||||
request, response = sanic_endpoint_test(app, data=payload, headers=headers)
|
request, response = sanic_endpoint_test(app, data=payload, headers=headers)
|
||||||
|
|
||||||
assert request.form.get('test') == 'OK'
|
assert request.form.get('test') == 'OK'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def redirect_app():
|
||||||
|
app = Sanic('test_redirection')
|
||||||
|
|
||||||
|
@app.route('/redirect_init')
|
||||||
|
async def redirect_init(request):
|
||||||
|
return redirect("/redirect_target")
|
||||||
|
|
||||||
|
@app.route('/redirect_init_with_301')
|
||||||
|
async def redirect_init_with_301(request):
|
||||||
|
return redirect("/redirect_target", status=301)
|
||||||
|
|
||||||
|
@app.route('/redirect_target')
|
||||||
|
async def redirect_target(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def test_redirect_default_302(redirect_app):
|
||||||
|
"""
|
||||||
|
We expect a 302 default status code and the headers to be set.
|
||||||
|
"""
|
||||||
|
request, response = sanic_endpoint_test(
|
||||||
|
redirect_app, method="get",
|
||||||
|
uri="/redirect_init",
|
||||||
|
allow_redirects=False)
|
||||||
|
|
||||||
|
assert response.status == 302
|
||||||
|
assert response.headers["Location"] == "/redirect_target"
|
||||||
|
assert response.headers["Content-Type"] == 'text/html; charset=utf-8'
|
||||||
|
|
||||||
|
|
||||||
|
def test_redirect_headers_none(redirect_app):
|
||||||
|
request, response = sanic_endpoint_test(
|
||||||
|
redirect_app, method="get",
|
||||||
|
uri="/redirect_init",
|
||||||
|
headers=None,
|
||||||
|
allow_redirects=False)
|
||||||
|
|
||||||
|
assert response.status == 302
|
||||||
|
assert response.headers["Location"] == "/redirect_target"
|
||||||
|
|
||||||
|
|
||||||
|
def test_redirect_with_301(redirect_app):
|
||||||
|
"""
|
||||||
|
Test redirection with a different status code.
|
||||||
|
"""
|
||||||
|
request, response = sanic_endpoint_test(
|
||||||
|
redirect_app, method="get",
|
||||||
|
uri="/redirect_init_with_301",
|
||||||
|
allow_redirects=False)
|
||||||
|
|
||||||
|
assert response.status == 301
|
||||||
|
assert response.headers["Location"] == "/redirect_target"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_then_redirect_follow_redirect(redirect_app):
|
||||||
|
"""
|
||||||
|
With `allow_redirects` we expect a 200.
|
||||||
|
"""
|
||||||
|
response = sanic_endpoint_test(
|
||||||
|
redirect_app, method="get",
|
||||||
|
uri="/redirect_init", gather_request=False,
|
||||||
|
allow_redirects=True)
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.text == 'OK'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.router import RouteExists
|
from sanic.router import RouteExists, RouteDoesNotExist
|
||||||
from sanic.utils import sanic_endpoint_test
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
|
|
||||||
|
@ -356,3 +356,110 @@ def test_add_route_method_not_allowed():
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app, method='post', uri='/test')
|
request, response = sanic_endpoint_test(app, method='post', uri='/test')
|
||||||
assert response.status == 405
|
assert response.status == 405
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_static_route():
|
||||||
|
app = Sanic('test_remove_static_route')
|
||||||
|
|
||||||
|
async def handler1(request):
|
||||||
|
return text('OK1')
|
||||||
|
|
||||||
|
async def handler2(request):
|
||||||
|
return text('OK2')
|
||||||
|
|
||||||
|
app.add_route(handler1, '/test')
|
||||||
|
app.add_route(handler2, '/test2')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test2')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
app.remove_route('/test')
|
||||||
|
app.remove_route('/test2')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test2')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_dynamic_route():
|
||||||
|
app = Sanic('test_remove_dynamic_route')
|
||||||
|
|
||||||
|
async def handler(request, name):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, '/folder/<name>')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
app.remove_route('/folder/<name>')
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_inexistent_route():
|
||||||
|
app = Sanic('test_remove_inexistent_route')
|
||||||
|
|
||||||
|
with pytest.raises(RouteDoesNotExist):
|
||||||
|
app.remove_route('/test')
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_unhashable_route():
|
||||||
|
app = Sanic('test_remove_unhashable_route')
|
||||||
|
|
||||||
|
async def handler(request, unhashable):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
app.remove_route('/folder/<unhashable:[A-Za-z0-9/]+>/end/')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_route_without_clean_cache():
|
||||||
|
app = Sanic('test_remove_static_route')
|
||||||
|
|
||||||
|
async def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, '/test')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
app.remove_route('/test', clean_cache=True)
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
app.add_route(handler, '/test')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
app.remove_route('/test', clean_cache=False)
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 200
|
||||||
|
|
59
tests/test_server_events.py
Normal file
59
tests/test_server_events.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
from io import StringIO
|
||||||
|
from random import choice
|
||||||
|
from string import ascii_letters
|
||||||
|
import signal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
AVAILABLE_LISTENERS = [
|
||||||
|
'before_start',
|
||||||
|
'after_start',
|
||||||
|
'before_stop',
|
||||||
|
'after_stop'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def create_listener(listener_name, in_list):
|
||||||
|
async def _listener(app, loop):
|
||||||
|
print('DEBUG MESSAGE FOR PYTEST for {}'.format(listener_name))
|
||||||
|
in_list.insert(0, app.name + listener_name)
|
||||||
|
return _listener
|
||||||
|
|
||||||
|
|
||||||
|
def start_stop_app(random_name_app, **run_kwargs):
|
||||||
|
|
||||||
|
def stop_on_alarm(signum, frame):
|
||||||
|
raise KeyboardInterrupt('SIGINT for sanic to stop gracefully')
|
||||||
|
|
||||||
|
signal.signal(signal.SIGALRM, stop_on_alarm)
|
||||||
|
signal.alarm(1)
|
||||||
|
try:
|
||||||
|
random_name_app.run(**run_kwargs)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS)
|
||||||
|
def test_single_listener(listener_name):
|
||||||
|
"""Test that listeners on their own work"""
|
||||||
|
random_name_app = Sanic(''.join(
|
||||||
|
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
|
||||||
|
output = list()
|
||||||
|
start_stop_app(
|
||||||
|
random_name_app,
|
||||||
|
**{listener_name: create_listener(listener_name, output)})
|
||||||
|
assert random_name_app.name + listener_name == output.pop()
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_listeners():
|
||||||
|
random_name_app = Sanic(''.join(
|
||||||
|
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
|
||||||
|
output = list()
|
||||||
|
start_stop_app(
|
||||||
|
random_name_app,
|
||||||
|
**{listener_name: create_listener(listener_name, output)
|
||||||
|
for listener_name in AVAILABLE_LISTENERS})
|
||||||
|
for listener_name in AVAILABLE_LISTENERS:
|
||||||
|
assert random_name_app.name + listener_name == output.pop()
|
23
tests/test_vhosts.py
Normal file
23
tests/test_vhosts.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json, text
|
||||||
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
|
|
||||||
|
def test_vhosts():
|
||||||
|
app = Sanic('test_text')
|
||||||
|
|
||||||
|
@app.route('/', host="example.com")
|
||||||
|
async def handler(request):
|
||||||
|
return text("You're at example.com!")
|
||||||
|
|
||||||
|
@app.route('/', host="subdomain.example.com")
|
||||||
|
async def handler(request):
|
||||||
|
return text("You're at subdomain.example.com!")
|
||||||
|
|
||||||
|
headers = {"Host": "example.com"}
|
||||||
|
request, response = sanic_endpoint_test(app, headers=headers)
|
||||||
|
assert response.text == "You're at example.com!"
|
||||||
|
|
||||||
|
headers = {"Host": "subdomain.example.com"}
|
||||||
|
request, response = sanic_endpoint_test(app, headers=headers)
|
||||||
|
assert response.text == "You're at subdomain.example.com!"
|
|
@ -26,7 +26,7 @@ def test_methods():
|
||||||
def delete(self, request):
|
def delete(self, request):
|
||||||
return text('I am delete method')
|
return text('I am delete method')
|
||||||
|
|
||||||
app.add_route(DummyView(), '/')
|
app.add_route(DummyView.as_view(), '/')
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app, method="get")
|
request, response = sanic_endpoint_test(app, method="get")
|
||||||
assert response.text == 'I am get method'
|
assert response.text == 'I am get method'
|
||||||
|
@ -48,7 +48,7 @@ def test_unexisting_methods():
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return text('I am get method')
|
return text('I am get method')
|
||||||
|
|
||||||
app.add_route(DummyView(), '/')
|
app.add_route(DummyView.as_view(), '/')
|
||||||
request, response = sanic_endpoint_test(app, method="get")
|
request, response = sanic_endpoint_test(app, method="get")
|
||||||
assert response.text == 'I am get method'
|
assert response.text == 'I am get method'
|
||||||
request, response = sanic_endpoint_test(app, method="post")
|
request, response = sanic_endpoint_test(app, method="post")
|
||||||
|
@ -63,7 +63,7 @@ def test_argument_methods():
|
||||||
def get(self, request, my_param_here):
|
def get(self, request, my_param_here):
|
||||||
return text('I am get method with %s' % my_param_here)
|
return text('I am get method with %s' % my_param_here)
|
||||||
|
|
||||||
app.add_route(DummyView(), '/<my_param_here>')
|
app.add_route(DummyView.as_view(), '/<my_param_here>')
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app, uri='/test123')
|
request, response = sanic_endpoint_test(app, uri='/test123')
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ def test_with_bp():
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return text('I am get method')
|
return text('I am get method')
|
||||||
|
|
||||||
bp.add_route(DummyView(), '/')
|
bp.add_route(DummyView.as_view(), '/')
|
||||||
|
|
||||||
app.blueprint(bp)
|
app.blueprint(bp)
|
||||||
request, response = sanic_endpoint_test(app)
|
request, response = sanic_endpoint_test(app)
|
||||||
|
@ -96,7 +96,7 @@ def test_with_bp_with_url_prefix():
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return text('I am get method')
|
return text('I am get method')
|
||||||
|
|
||||||
bp.add_route(DummyView(), '/')
|
bp.add_route(DummyView.as_view(), '/')
|
||||||
|
|
||||||
app.blueprint(bp)
|
app.blueprint(bp)
|
||||||
request, response = sanic_endpoint_test(app, uri='/test1/')
|
request, response = sanic_endpoint_test(app, uri='/test1/')
|
||||||
|
@ -112,7 +112,7 @@ def test_with_middleware():
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return text('I am get method')
|
return text('I am get method')
|
||||||
|
|
||||||
app.add_route(DummyView(), '/')
|
app.add_route(DummyView.as_view(), '/')
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ def test_with_middleware_response():
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return text('I am get method')
|
return text('I am get method')
|
||||||
|
|
||||||
app.add_route(DummyView(), '/')
|
app.add_route(DummyView.as_view(), '/')
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app)
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
@ -153,3 +153,44 @@ def test_with_middleware_response():
|
||||||
assert type(results[0]) is Request
|
assert type(results[0]) is Request
|
||||||
assert type(results[1]) is Request
|
assert type(results[1]) is Request
|
||||||
assert issubclass(type(results[2]), HTTPResponse)
|
assert issubclass(type(results[2]), HTTPResponse)
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_custom_class_methods():
|
||||||
|
app = Sanic('test_with_custom_class_methods')
|
||||||
|
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
|
global_var = 0
|
||||||
|
|
||||||
|
def _iternal_method(self):
|
||||||
|
self.global_var += 10
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
self._iternal_method()
|
||||||
|
return text('I am get method and global var is {}'.format(self.global_var))
|
||||||
|
|
||||||
|
app.add_route(DummyView.as_view(), '/')
|
||||||
|
request, response = sanic_endpoint_test(app, method="get")
|
||||||
|
assert response.text == 'I am get method and global var is 10'
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_decorator():
|
||||||
|
app = Sanic('test_with_decorator')
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
def stupid_decorator(view):
|
||||||
|
def decorator(*args, **kwargs):
|
||||||
|
results.append(1)
|
||||||
|
return view(*args, **kwargs)
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
|
decorators = [stupid_decorator]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return text('I am get method')
|
||||||
|
|
||||||
|
app.add_route(DummyView.as_view(), '/')
|
||||||
|
request, response = sanic_endpoint_test(app, method="get")
|
||||||
|
assert response.text == 'I am get method'
|
||||||
|
assert results[0] == 1
|
||||||
|
|
26
tox.ini
26
tox.ini
|
@ -1,22 +1,22 @@
|
||||||
[tox]
|
[tox]
|
||||||
|
|
||||||
envlist = py35, py36
|
envlist = py35, py36, flake8
|
||||||
|
|
||||||
|
[travis]
|
||||||
|
|
||||||
|
python =
|
||||||
|
3.5: py35, flake8
|
||||||
|
3.6: py36, flake8
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
|
||||||
deps =
|
deps =
|
||||||
aiohttp
|
aiohttp
|
||||||
pytest
|
pytest
|
||||||
coverage
|
beautifulsoup4
|
||||||
|
|
||||||
commands =
|
commands =
|
||||||
coverage run -m pytest -v tests {posargs}
|
pytest tests {posargs}
|
||||||
mv .coverage .coverage.{envname}
|
|
||||||
|
|
||||||
whitelist_externals =
|
|
||||||
coverage
|
|
||||||
mv
|
|
||||||
echo
|
|
||||||
|
|
||||||
[testenv:flake8]
|
[testenv:flake8]
|
||||||
deps =
|
deps =
|
||||||
|
@ -24,11 +24,3 @@ deps =
|
||||||
|
|
||||||
commands =
|
commands =
|
||||||
flake8 sanic
|
flake8 sanic
|
||||||
|
|
||||||
[testenv:report]
|
|
||||||
|
|
||||||
commands =
|
|
||||||
coverage combine
|
|
||||||
coverage report
|
|
||||||
coverage html
|
|
||||||
echo "Open file://{toxinidir}/coverage/index.html"
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user