Merge branch 'master' into improved_config
This commit is contained in:
commit
0b9094d348
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,11 +1,13 @@
|
|||
*~
|
||||
*.egg-info
|
||||
*.egg
|
||||
*.eggs
|
||||
*.pyc
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage
|
||||
.tox
|
||||
settings.py
|
||||
*.pyc
|
||||
.idea/*
|
||||
.cache/*
|
||||
.python-version
|
||||
|
|
11
.travis.yml
11
.travis.yml
|
@ -1,15 +1,10 @@
|
|||
sudo: false
|
||||
language: python
|
||||
python:
|
||||
- '3.5'
|
||||
- '3.6'
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- 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
|
||||
install: pip install tox-travis
|
||||
script: tox
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: channelcat
|
||||
|
|
|
@ -60,6 +60,7 @@ if __name__ == "__main__":
|
|||
* [Cookies](docs/cookies.md)
|
||||
* [Static Files](docs/static_files.md)
|
||||
* [Configuration](docs/config.md)
|
||||
* [Custom Protocol](docs/custom_protocol.md)
|
||||
* [Testing](docs/testing.md)
|
||||
* [Deploying](docs/deploying.md)
|
||||
* [Contributing](docs/contributing.md)
|
||||
|
|
|
@ -28,7 +28,7 @@ class SimpleView(HTTPMethodView):
|
|||
def delete(self, request):
|
||||
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):
|
||||
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)
|
||||
```
|
|
@ -33,12 +33,12 @@ async def handler1(request):
|
|||
return text('OK')
|
||||
app.add_route(handler1, '/test')
|
||||
|
||||
async def handler(request, name):
|
||||
async def handler2(request, 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))
|
||||
app.add_route(handler, '/person/<name:[A-z]>')
|
||||
app.add_route(person_handler2, '/person/<name:[A-z]>')
|
||||
|
||||
```
|
||||
|
|
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
|
||||
# ----------------------------------------------- #
|
||||
|
||||
def after_start(loop):
|
||||
def after_start(app, loop):
|
||||
log.info("OH OH OH OH OHHHHHHHH")
|
||||
|
||||
|
||||
def before_stop(loop):
|
||||
def before_stop(app, loop):
|
||||
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)
|
|
@ -20,7 +20,7 @@ if __name__ == "__main__":
|
|||
|
||||
module = import_module(module_name)
|
||||
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 {}. "
|
||||
"Perhaps you meant {}.app?"
|
||||
.format(type(app).__name__, args.module))
|
||||
|
|
|
@ -18,14 +18,17 @@ class BlueprintSetup:
|
|||
#: blueprint.
|
||||
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.
|
||||
"""
|
||||
if self.url_prefix:
|
||||
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):
|
||||
"""
|
||||
|
@ -53,7 +56,7 @@ class BlueprintSetup:
|
|||
|
||||
|
||||
class Blueprint:
|
||||
def __init__(self, name, url_prefix=None):
|
||||
def __init__(self, name, url_prefix=None, host=None):
|
||||
"""
|
||||
Creates a new blueprint
|
||||
:param name: Unique name of the blueprint
|
||||
|
@ -63,6 +66,7 @@ class Blueprint:
|
|||
self.url_prefix = url_prefix
|
||||
self.deferred_functions = []
|
||||
self.listeners = defaultdict(list)
|
||||
self.host = host
|
||||
|
||||
def record(self, func):
|
||||
"""
|
||||
|
@ -83,18 +87,18 @@ class Blueprint:
|
|||
for deferred in self.deferred_functions:
|
||||
deferred(state)
|
||||
|
||||
def route(self, uri, methods=None):
|
||||
def route(self, uri, methods=None, host=None):
|
||||
"""
|
||||
"""
|
||||
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 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
|
||||
|
||||
def listener(self, event):
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from .response import text
|
||||
from .log import log
|
||||
from traceback import format_exc
|
||||
|
||||
|
||||
|
@ -56,18 +57,31 @@ class Handler:
|
|||
:return: Response object
|
||||
"""
|
||||
handler = self.handlers.get(type(exception), self.default)
|
||||
try:
|
||||
response = handler(request=request, exception=exception)
|
||||
except:
|
||||
if self.sanic.debug:
|
||||
response_message = (
|
||||
'Exception raised in exception handler "{}" '
|
||||
'for uri: "{}"\n{}').format(
|
||||
handler.__name__, request.url, format_exc())
|
||||
log.error(response_message)
|
||||
return text(response_message, 500)
|
||||
else:
|
||||
return text('An error occurred while handling an error', 500)
|
||||
return response
|
||||
|
||||
def default(self, request, exception):
|
||||
if issubclass(type(exception), SanicException):
|
||||
return text(
|
||||
"Error: {}".format(exception),
|
||||
'Error: {}'.format(exception),
|
||||
status=getattr(exception, 'status_code', 500))
|
||||
elif self.sanic.debug:
|
||||
return text(
|
||||
"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 text(response_message, status=500)
|
||||
else:
|
||||
return text(
|
||||
"An error occurred while generating the request", status=500)
|
||||
'An error occurred while generating the response', status=500)
|
||||
|
|
|
@ -25,6 +25,9 @@ class RequestParameters(dict):
|
|||
self.super = super()
|
||||
self.super.__init__(*args, **kwargs)
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self.get(name)
|
||||
|
||||
def get(self, name, default=None):
|
||||
values = self.super.get(name)
|
||||
return values[0] if values else default
|
||||
|
@ -64,7 +67,7 @@ class Request(dict):
|
|||
|
||||
@property
|
||||
def json(self):
|
||||
if not self.parsed_json:
|
||||
if self.parsed_json is None:
|
||||
try:
|
||||
self.parsed_json = json_loads(self.body)
|
||||
except Exception:
|
||||
|
@ -72,6 +75,17 @@ class Request(dict):
|
|||
|
||||
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
|
||||
def form(self):
|
||||
if self.parsed_form is None:
|
||||
|
|
|
@ -103,10 +103,14 @@ class HTTPResponse:
|
|||
|
||||
headers = b''
|
||||
if self.headers:
|
||||
headers = b''.join(
|
||||
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
|
||||
for name, value in self.headers.items()
|
||||
)
|
||||
for name, value in self.headers.items():
|
||||
try:
|
||||
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
|
||||
# Speeds up response rate 6% over pulling from all
|
||||
|
|
|
@ -24,16 +24,20 @@ class RouteExists(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class RouteDoesNotExist(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Router:
|
||||
"""
|
||||
Router supports basic routing with parameters and method checks
|
||||
Usage:
|
||||
@sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...])
|
||||
def my_route(request, my_parameter):
|
||||
@app.route('/my_url/<my_param>', methods=['GET', 'POST', ...])
|
||||
def my_route(request, my_param):
|
||||
do stuff...
|
||||
or
|
||||
@sanic.route('/my/url/<my_paramter>:type', methods['GET', 'POST', ...])
|
||||
def my_route_with_type(request, my_parameter):
|
||||
@app.route('/my_url/<my_param:my_type>', methods=['GET', 'POST', ...])
|
||||
def my_route_with_type(request, my_param: my_type):
|
||||
do stuff...
|
||||
|
||||
Parameters will be passed as keyword arguments to the request handling
|
||||
|
@ -52,8 +56,9 @@ class Router:
|
|||
self.routes_static = {}
|
||||
self.routes_dynamic = defaultdict(list)
|
||||
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
|
||||
:param uri: Path to match
|
||||
|
@ -63,6 +68,17 @@ class Router:
|
|||
When executed, it should provide a response object.
|
||||
: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:
|
||||
raise RouteExists("Route already registered: {}".format(uri))
|
||||
|
||||
|
@ -110,6 +126,25 @@ class Router:
|
|||
else:
|
||||
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):
|
||||
"""
|
||||
Gets a request handler based on the URL of the request, or raises an
|
||||
|
@ -117,10 +152,14 @@ class Router:
|
|||
:param request: Request object
|
||||
:return: handler, arguments, keyword arguments
|
||||
"""
|
||||
return self._get(request.url, request.method)
|
||||
if self.hosts is None:
|
||||
return self._get(request.url, request.method, '')
|
||||
else:
|
||||
return self._get(request.url, request.method,
|
||||
request.headers.get("Host", ''))
|
||||
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
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
|
||||
error. Internal method for caching.
|
||||
|
@ -128,6 +167,7 @@ class Router:
|
|||
:param method: Request method
|
||||
:return: handler, arguments, keyword arguments
|
||||
"""
|
||||
url = host + url
|
||||
# Check against known static routes
|
||||
route = self.routes_static.get(url)
|
||||
if route:
|
||||
|
|
|
@ -4,7 +4,6 @@ from functools import partial
|
|||
from inspect import isawaitable, stack, getmodulename
|
||||
from multiprocessing import Process, Event
|
||||
from signal import signal, SIGTERM, SIGINT
|
||||
from time import sleep
|
||||
from traceback import format_exc
|
||||
import logging
|
||||
|
||||
|
@ -13,9 +12,11 @@ from .exceptions import Handler
|
|||
from .log import log
|
||||
from .response import HTTPResponse
|
||||
from .router import Router
|
||||
from .server import serve
|
||||
from .server import serve, HttpProtocol
|
||||
from .static import register as static_register
|
||||
from .exceptions import ServerError
|
||||
from socket import socket, SOL_SOCKET, SO_REUSEADDR
|
||||
from os import set_inheritable
|
||||
|
||||
|
||||
class Sanic:
|
||||
|
@ -39,6 +40,8 @@ class Sanic:
|
|||
self._blueprint_order = []
|
||||
self.loop = None
|
||||
self.debug = None
|
||||
self.sock = None
|
||||
self.processes = None
|
||||
|
||||
# Register alternative method names
|
||||
self.go_fast = self.run
|
||||
|
@ -48,7 +51,7 @@ class Sanic:
|
|||
# -------------------------------------------------------------------- #
|
||||
|
||||
# Decorator
|
||||
def route(self, uri, methods=None):
|
||||
def route(self, uri, methods=None, host=None):
|
||||
"""
|
||||
Decorates a function to be registered as a route
|
||||
:param uri: path of the URL
|
||||
|
@ -62,12 +65,13 @@ class Sanic:
|
|||
uri = '/' + uri
|
||||
|
||||
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 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
|
||||
functions as a handler to the application url
|
||||
|
@ -77,9 +81,12 @@ class Sanic:
|
|||
:param methods: list or tuple of methods allowed
|
||||
:return: function or class instance
|
||||
"""
|
||||
self.route(uri=uri, methods=methods)(handler)
|
||||
self.route(uri=uri, methods=methods, host=host)(handler)
|
||||
return handler
|
||||
|
||||
def remove_route(self, uri, clean_cache=True, host=None):
|
||||
self.router.remove(uri, clean_cache, host)
|
||||
|
||||
# Decorator
|
||||
def exception(self, *exceptions):
|
||||
"""
|
||||
|
@ -239,25 +246,27 @@ class Sanic:
|
|||
|
||||
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
|
||||
after_start=None, before_stop=None, after_stop=None, sock=None,
|
||||
workers=1, loop=None):
|
||||
workers=1, loop=None, protocol=HttpProtocol, backlog=100,
|
||||
stop_event=None):
|
||||
"""
|
||||
Runs the HTTP Server and listens until keyboard interrupt or term
|
||||
signal. On termination, drains connections before closing.
|
||||
:param host: Address to host on
|
||||
:param port: Port to host on
|
||||
: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
|
||||
:param after_start: Function to be executed after the server starts
|
||||
:param after_start: Functions to be executed after the server starts
|
||||
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
|
||||
:param after_stop: Function to be executed when all requests are
|
||||
:param after_stop: Functions to be executed when all requests are
|
||||
complete
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:param workers: Number of processes
|
||||
received before it is respected
|
||||
:param loop: asyncio compatible event loop
|
||||
:param protocol: Subclass of asyncio protocol class
|
||||
:return: Nothing
|
||||
"""
|
||||
self.error_handler.debug = True
|
||||
|
@ -265,6 +274,7 @@ class Sanic:
|
|||
self.loop = loop
|
||||
|
||||
server_settings = {
|
||||
'protocol': protocol,
|
||||
'host': host,
|
||||
'port': port,
|
||||
'sock': sock,
|
||||
|
@ -273,7 +283,8 @@ class Sanic:
|
|||
'error_handler': self.error_handler,
|
||||
'request_timeout': self.config.REQUEST_TIMEOUT,
|
||||
'request_max_size': self.config.REQUEST_MAX_SIZE,
|
||||
'loop': loop
|
||||
'loop': loop,
|
||||
'backlog': backlog
|
||||
}
|
||||
|
||||
# -------------------------------------------- #
|
||||
|
@ -290,7 +301,7 @@ class Sanic:
|
|||
for blueprint in self.blueprints.values():
|
||||
listeners += blueprint.listeners[event_name]
|
||||
if args:
|
||||
if type(args) is not list:
|
||||
if callable(args):
|
||||
args = [args]
|
||||
listeners += args
|
||||
if reverse:
|
||||
|
@ -312,7 +323,7 @@ class Sanic:
|
|||
else:
|
||||
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:
|
||||
log.exception(
|
||||
|
@ -324,10 +335,13 @@ class 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()
|
||||
|
||||
@staticmethod
|
||||
def serve_multiple(server_settings, workers, stop_event=None):
|
||||
def serve_multiple(self, server_settings, workers, stop_event=None):
|
||||
"""
|
||||
Starts multiple server processes simultaneously. Stops on interrupt
|
||||
and terminate signals, and drains connections when complete.
|
||||
|
@ -339,26 +353,28 @@ class Sanic:
|
|||
server_settings['reuse_port'] = True
|
||||
|
||||
# Create a stop event to be triggered by a signal
|
||||
if not stop_event:
|
||||
if stop_event is None:
|
||||
stop_event = Event()
|
||||
signal(SIGINT, 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):
|
||||
process = Process(target=serve, kwargs=server_settings)
|
||||
process.daemon = True
|
||||
process.start()
|
||||
processes.append(process)
|
||||
self.processes.append(process)
|
||||
|
||||
# Infinitely wait for the stop event
|
||||
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:
|
||||
for process in self.processes:
|
||||
process.join()
|
||||
|
||||
# the above processes will block this until they're stopped
|
||||
self.stop()
|
||||
|
|
|
@ -224,24 +224,30 @@ def trigger_events(events, loop):
|
|||
|
||||
|
||||
def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
after_start=None, before_stop=None, after_stop=None,
|
||||
debug=False, request_timeout=60, sock=None,
|
||||
request_max_size=None, reuse_port=False, loop=None):
|
||||
after_start=None, before_stop=None, after_stop=None, debug=False,
|
||||
request_timeout=60, sock=None, request_max_size=None,
|
||||
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100):
|
||||
"""
|
||||
Starts asynchronous HTTP Server on an individual process.
|
||||
:param host: Address to host on
|
||||
:param port: Port to host on
|
||||
: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
|
||||
listening. Takes single argument `loop`
|
||||
:param before_stop: Function to be executed when a stop signal is
|
||||
received before it is respected. Takes single argumenet `loop`
|
||||
:param after_stop: Function to be executed when a stop signal is
|
||||
received after it is respected. Takes single argumenet `loop`
|
||||
:param debug: Enables debug output (slows server)
|
||||
:param request_timeout: time in seconds
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:param request_max_size: size in bytes, `None` for no limit
|
||||
:param reuse_port: `True` for multiple workers
|
||||
:param loop: asyncio compatible event loop
|
||||
:param protocol: Subclass of asyncio protocol class
|
||||
:return: Nothing
|
||||
"""
|
||||
loop = loop or async_loop.new_event_loop()
|
||||
|
@ -255,7 +261,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
|||
connections = set()
|
||||
signal = Signal()
|
||||
server = partial(
|
||||
HttpProtocol,
|
||||
protocol,
|
||||
loop=loop,
|
||||
connections=connections,
|
||||
signal=signal,
|
||||
|
@ -270,7 +276,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
|||
host,
|
||||
port,
|
||||
reuse_port=reuse_port,
|
||||
sock=sock
|
||||
sock=sock,
|
||||
backlog=backlog
|
||||
)
|
||||
|
||||
# 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,
|
||||
loop=None, debug=False, *request_args,
|
||||
**request_kwargs):
|
||||
loop=None, debug=False, server_kwargs={},
|
||||
*request_args, **request_kwargs):
|
||||
results = []
|
||||
exceptions = []
|
||||
|
||||
if gather_request:
|
||||
@app.middleware
|
||||
def _collect_request(request):
|
||||
results.append(request)
|
||||
app.request_middleware.appendleft(_collect_request)
|
||||
|
||||
async def _collect_response(sanic, loop):
|
||||
try:
|
||||
|
@ -35,8 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
|||
exceptions.append(e)
|
||||
app.stop()
|
||||
|
||||
app.run(host=HOST, debug=debug, port=42101,
|
||||
after_start=_collect_response, loop=loop)
|
||||
app.run(host=HOST, debug=debug, port=PORT,
|
||||
after_start=_collect_response, loop=loop, **server_kwargs)
|
||||
|
||||
if exceptions:
|
||||
raise ValueError("Exception during request: {}".format(exceptions))
|
||||
|
|
|
@ -7,7 +7,7 @@ class HTTPMethodView:
|
|||
to every HTTP method you want to support.
|
||||
|
||||
For example:
|
||||
class DummyView(View):
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return text('I am get method')
|
||||
|
@ -20,20 +20,44 @@ class HTTPMethodView:
|
|||
405 response.
|
||||
|
||||
If you need any url params just mention them in method definition:
|
||||
class DummyView(View):
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
def get(self, request, my_param_here, *args, **kwargs):
|
||||
return text('I am get method with %s' % my_param_here)
|
||||
|
||||
To add the view into the routing you could use
|
||||
1) app.add_route(DummyView(), '/')
|
||||
2) app.route('/')(DummyView())
|
||||
1) app.add_route(DummyView.as_view(), '/')
|
||||
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)
|
||||
if handler:
|
||||
return handler(request, *args, **kwargs)
|
||||
raise InvalidUsage(
|
||||
'Method {} not allowed for URL {}'.format(
|
||||
request.method, request.url), status_code=405)
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, *class_args, **class_kwargs):
|
||||
""" Converts the class into an actual view function that can be used
|
||||
with the routing system.
|
||||
|
||||
"""
|
||||
def view(*args, **kwargs):
|
||||
self = view.view_class(*class_args, **class_kwargs)
|
||||
return self.dispatch_request(*args, **kwargs)
|
||||
|
||||
if cls.decorators:
|
||||
view.__module__ = cls.__module__
|
||||
for decorator in cls.decorators:
|
||||
view = decorator(view)
|
||||
|
||||
view.view_class = cls
|
||||
view.__doc__ = cls.__doc__
|
||||
view.__module__ = cls.__module__
|
||||
return view
|
||||
|
|
|
@ -59,6 +59,71 @@ def test_several_bp_with_url_prefix():
|
|||
request, response = sanic_endpoint_test(app, uri='/test2/')
|
||||
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():
|
||||
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,86 @@
|
|||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
exception_app = Sanic('test_exceptions')
|
||||
class SanicExceptionTestException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@exception_app.route('/')
|
||||
def handler(request):
|
||||
@pytest.fixture(scope='module')
|
||||
def exception_app():
|
||||
app = Sanic('test_exceptions')
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
|
||||
@exception_app.route('/error')
|
||||
def handler_error(request):
|
||||
@app.route('/error')
|
||||
def handler_error(request):
|
||||
raise ServerError("OK")
|
||||
|
||||
|
||||
@exception_app.route('/404')
|
||||
def handler_404(request):
|
||||
@app.route('/404')
|
||||
def handler_404(request):
|
||||
raise NotFound("OK")
|
||||
|
||||
|
||||
@exception_app.route('/invalid')
|
||||
def handler_invalid(request):
|
||||
@app.route('/invalid')
|
||||
def handler_invalid(request):
|
||||
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)
|
||||
assert response.status == 200
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_handled_unhandled_exception(exception_app):
|
||||
"""Test that an exception not built into sanic is handled"""
|
||||
request, response = sanic_endpoint_test(
|
||||
exception_app, uri='/divide_by_zero')
|
||||
assert response.status == 500
|
||||
assert response.body == b'An error occurred while generating the response'
|
||||
|
||||
|
||||
def test_exception_in_exception_handler(exception_app):
|
||||
"""Test that an exception thrown in an error handler is handled"""
|
||||
request, response = sanic_endpoint_test(
|
||||
exception_app, uri='/error_in_error_handler_handler')
|
||||
assert response.status == 500
|
||||
assert response.body == b'An error occurred while handling an error'
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from multiprocessing import Array, Event, Process
|
||||
from time import sleep
|
||||
from time import sleep, time
|
||||
from ujson import loads as json_loads
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
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
|
||||
# executed via interpreter
|
||||
|
||||
def skip_test_multiprocessing():
|
||||
@pytest.mark.skip(
|
||||
reason="Freezes with pytest not on interpreter")
|
||||
def test_multiprocessing():
|
||||
app = Sanic('test_json')
|
||||
|
||||
response = Array('c', 50)
|
||||
|
@ -51,3 +54,28 @@ def skip_test_multiprocessing():
|
|||
raise ValueError("Expected JSON response but got '{}'".format(response))
|
||||
|
||||
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
|
||||
|
|
|
@ -33,6 +33,31 @@ def test_text():
|
|||
assert response.text == 'Hello'
|
||||
|
||||
|
||||
def test_headers():
|
||||
app = Sanic('test_text')
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
headers = {"spam": "great"}
|
||||
return text('Hello', headers=headers)
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
|
||||
assert response.headers.get('spam') == 'great'
|
||||
|
||||
|
||||
def test_non_str_headers():
|
||||
app = Sanic('test_text')
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
headers = {"answer": 42}
|
||||
return text('Hello', headers=headers)
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
|
||||
assert response.headers.get('answer') == '42'
|
||||
|
||||
def test_invalid_response():
|
||||
app = Sanic('test_invalid_response')
|
||||
|
||||
|
@ -92,6 +117,24 @@ def test_query_string():
|
|||
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
|
||||
# ------------------------------------------------------------ #
|
||||
|
|
|
@ -2,7 +2,7 @@ import pytest
|
|||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.router import RouteExists
|
||||
from sanic.router import RouteExists, RouteDoesNotExist
|
||||
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')
|
||||
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):
|
||||
return text('I am delete method')
|
||||
|
||||
app.add_route(DummyView(), '/')
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
|
||||
request, response = sanic_endpoint_test(app, method="get")
|
||||
assert response.text == 'I am get method'
|
||||
|
@ -48,7 +48,7 @@ def test_unexisting_methods():
|
|||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
app.add_route(DummyView(), '/')
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
request, response = sanic_endpoint_test(app, method="get")
|
||||
assert response.text == 'I am get method'
|
||||
request, response = sanic_endpoint_test(app, method="post")
|
||||
|
@ -63,7 +63,7 @@ def test_argument_methods():
|
|||
def get(self, request, 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')
|
||||
|
||||
|
@ -79,7 +79,7 @@ def test_with_bp():
|
|||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
bp.add_route(DummyView(), '/')
|
||||
bp.add_route(DummyView.as_view(), '/')
|
||||
|
||||
app.blueprint(bp)
|
||||
request, response = sanic_endpoint_test(app)
|
||||
|
@ -96,7 +96,7 @@ def test_with_bp_with_url_prefix():
|
|||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
bp.add_route(DummyView(), '/')
|
||||
bp.add_route(DummyView.as_view(), '/')
|
||||
|
||||
app.blueprint(bp)
|
||||
request, response = sanic_endpoint_test(app, uri='/test1/')
|
||||
|
@ -112,7 +112,7 @@ def test_with_middleware():
|
|||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
app.add_route(DummyView(), '/')
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
|
||||
results = []
|
||||
|
||||
|
@ -145,7 +145,7 @@ def test_with_middleware_response():
|
|||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
app.add_route(DummyView(), '/')
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
|
||||
|
@ -153,3 +153,44 @@ def test_with_middleware_response():
|
|||
assert type(results[0]) is Request
|
||||
assert type(results[1]) is Request
|
||||
assert issubclass(type(results[2]), HTTPResponse)
|
||||
|
||||
|
||||
def test_with_custom_class_methods():
|
||||
app = Sanic('test_with_custom_class_methods')
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
global_var = 0
|
||||
|
||||
def _iternal_method(self):
|
||||
self.global_var += 10
|
||||
|
||||
def get(self, request):
|
||||
self._iternal_method()
|
||||
return text('I am get method and global var is {}'.format(self.global_var))
|
||||
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
request, response = sanic_endpoint_test(app, method="get")
|
||||
assert response.text == 'I am get method and global var is 10'
|
||||
|
||||
|
||||
def test_with_decorator():
|
||||
app = Sanic('test_with_decorator')
|
||||
|
||||
results = []
|
||||
|
||||
def stupid_decorator(view):
|
||||
def decorator(*args, **kwargs):
|
||||
results.append(1)
|
||||
return view(*args, **kwargs)
|
||||
return decorator
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
decorators = [stupid_decorator]
|
||||
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
request, response = sanic_endpoint_test(app, method="get")
|
||||
assert response.text == 'I am get method'
|
||||
assert results[0] == 1
|
||||
|
|
25
tox.ini
25
tox.ini
|
@ -1,22 +1,21 @@
|
|||
[tox]
|
||||
|
||||
envlist = py35, py36
|
||||
envlist = py35, py36, flake8
|
||||
|
||||
[travis]
|
||||
|
||||
python =
|
||||
3.5: py35, flake8
|
||||
3.6: py36, flake8
|
||||
|
||||
[testenv]
|
||||
|
||||
deps =
|
||||
aiohttp
|
||||
pytest
|
||||
coverage
|
||||
|
||||
commands =
|
||||
coverage run -m pytest -v tests {posargs}
|
||||
mv .coverage .coverage.{envname}
|
||||
|
||||
whitelist_externals =
|
||||
coverage
|
||||
mv
|
||||
echo
|
||||
pytest tests {posargs}
|
||||
|
||||
[testenv:flake8]
|
||||
deps =
|
||||
|
@ -24,11 +23,3 @@ deps =
|
|||
|
||||
commands =
|
||||
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