merge upstream

This commit is contained in:
ivan 2017-03-23 20:08:19 +08:00
commit be1016ace6
21 changed files with 488 additions and 112 deletions

View File

@ -22,7 +22,7 @@ import sanic
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
extensions = [] extensions = ['sphinx.ext.autodoc']
templates_path = ['_templates'] templates_path = ['_templates']
@ -68,7 +68,6 @@ pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing. # If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False todo_include_todos = False
# -- Options for HTML output ---------------------------------------------- # -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
@ -80,13 +79,11 @@ html_theme = 'sphinx_rtd_theme'
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static'] html_static_path = ['_static']
# -- Options for HTMLHelp output ------------------------------------------ # -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'Sanicdoc' htmlhelp_basename = 'Sanicdoc'
# -- Options for LaTeX output --------------------------------------------- # -- Options for LaTeX output ---------------------------------------------
latex_elements = { latex_elements = {
@ -110,21 +107,14 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [(master_doc, 'Sanic.tex', 'Sanic Documentation',
(master_doc, 'Sanic.tex', 'Sanic Documentation', 'Sanic contributors', 'manual'), ]
'Sanic contributors', 'manual'),
]
# -- Options for manual page output --------------------------------------- # -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [(master_doc, 'sanic', 'Sanic Documentation', [author], 1)]
(master_doc, 'sanic', 'Sanic Documentation',
[author], 1)
]
# -- Options for Texinfo output ------------------------------------------- # -- Options for Texinfo output -------------------------------------------
@ -132,13 +122,10 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
(master_doc, 'Sanic', 'Sanic Documentation', (master_doc, 'Sanic', 'Sanic Documentation', author, 'Sanic',
author, 'Sanic', 'One line description of project.', 'One line description of project.', 'Miscellaneous'),
'Miscellaneous'),
] ]
# -- Options for Epub output ---------------------------------------------- # -- Options for Epub output ----------------------------------------------
# Bibliographic Dublin Core info. # Bibliographic Dublin Core info.
@ -150,8 +137,6 @@ epub_copyright = copyright
# A list of files that should not be packed into the epub file. # A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html'] epub_exclude_files = ['search.html']
# -- Custom Settings ------------------------------------------------------- # -- Custom Settings -------------------------------------------------------
suppress_warnings = ['image.nonlocal_uri'] suppress_warnings = ['image.nonlocal_uri']

View File

@ -16,6 +16,7 @@ Guides
sanic/blueprints sanic/blueprints
sanic/config sanic/config
sanic/cookies sanic/cookies
sanic/streaming
sanic/class_based_views sanic/class_based_views
sanic/custom_protocol sanic/custom_protocol
sanic/ssl sanic/ssl

View File

@ -66,7 +66,7 @@ Using blueprints allows you to also register middleware globally.
```python ```python
@bp.middleware @bp.middleware
async def halt_request(request): async def print_on_request(request):
print("I am a spy") print("I am a spy")
@bp.middleware('request') @bp.middleware('request')

View File

@ -48,6 +48,24 @@ app.add_route(SimpleView.as_view(), '/')
``` ```
You can also use `async` syntax.
```python
from sanic import Sanic
from sanic.views import HTTPMethodView
from sanic.response import text
app = Sanic('some_name')
class SimpleAsyncView(HTTPMethodView):
async def get(self, request):
return text('I am async get method')
app.add_route(SimpleAsyncView.as_view(), '/')
```
## URL parameters ## URL parameters
If you need any URL parameters, as discussed in the routing guide, include them If you need any URL parameters, as discussed in the routing guide, include them

View File

@ -71,8 +71,7 @@ DB_USER = 'appuser'
Out of the box there are just a few predefined values which can be overwritten when creating the application. Out of the box there are just a few predefined values which can be overwritten when creating the application.
| Variable | Default | Description | | Variable | Default | Description |
| ----------------- | --------- | --------------------------------- | | ----------------- | --------- | --------------------------------- |
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) | | REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) | | REQUEST_TIMEOUT | 60 | How long a request can take (sec) |

View File

@ -17,3 +17,4 @@ A list of Sanic extensions created by the community.
- [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the - [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the
`Babel` library `Babel` library
- [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter. - [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter.
- [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers.

View File

@ -7,8 +7,8 @@ On top of being Flask-like, Sanic supports async request handlers. This means y
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome! Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
Sanic aspires to be simple: Sanic aspires to be simple
------------------- ---------------------------
.. code:: python .. code:: python

View File

@ -15,4 +15,5 @@ dependencies:
- httptools>=0.0.9 - httptools>=0.0.9
- ujson>=1.35 - ujson>=1.35
- aiofiles>=0.3.0 - aiofiles>=0.3.0
- websockets>=3.2
- https://github.com/channelcat/docutils-fork/zipball/master - https://github.com/channelcat/docutils-fork/zipball/master

View File

@ -0,0 +1,62 @@
# encoding: utf-8
"""
You need the aiomysql
"""
import os
import aiomysql
from sanic import Sanic
from sanic.response import json
database_name = os.environ['DATABASE_NAME']
database_host = os.environ['DATABASE_HOST']
database_user = os.environ['DATABASE_USER']
database_password = os.environ['DATABASE_PASSWORD']
app = Sanic()
@app.listener("before_server_start")
async def get_pool(app, loop):
"""
the first param is the global instance ,
so we can store our connection pool in it .
and it can be used by different request
:param args:
:param kwargs:
:return:
"""
app.pool = {
"aiomysql": await aiomysql.create_pool(host=database_host, user=database_user, password=database_password,
db=database_name,
maxsize=5)}
async with app.pool['aiomysql'].acquire() as conn:
async with conn.cursor() as cur:
await cur.execute('DROP TABLE IF EXISTS sanic_polls')
await cur.execute("""CREATE TABLE sanic_polls (
id serial primary key,
question varchar(50),
pub_date timestamp
);""")
for i in range(0, 100):
await cur.execute("""INSERT INTO sanic_polls
(id, question, pub_date) VALUES ({}, {}, now())
""".format(i, i))
@app.route("/")
async def test():
result = []
data = {}
async with app.pool['aiomysql'].acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("SELECT question, pub_date FROM sanic_polls")
async for row in cur:
result.append({"question": row[0], "pub_date": row[1]})
if result or len(result) > 0:
data['data'] = res
return json(data)
if __name__ == '__main__':
app.run(host="127.0.0.1", workers=4, port=12000)

View File

@ -0,0 +1,34 @@
""" To run this example you need additional aioredis package
"""
from sanic import Sanic, response
import aioredis
app = Sanic(__name__)
@app.route("/")
async def handle(request):
async with request.app.redis_pool.get() as redis:
await redis.set('test-my-key', 'value')
val = await redis.get('test-my-key')
return response.text(val.decode('utf-8'))
@app.listener('before_server_start')
async def before_server_start(app, loop):
app.redis_pool = await aioredis.create_pool(
('localhost', 6379),
minsize=5,
maxsize=10,
loop=loop
)
@app.listener('after_server_stop')
async def after_server_stop(app, loop):
app.redis_pool.close()
await app.redis_pool.wait_closed()
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@ -1,3 +1,4 @@
## You need the following additional packages for this example ## You need the following additional packages for this example
# aiopg # aiopg
# peewee_async # peewee_async
@ -10,8 +11,9 @@ from sanic.response import json
## peewee_async related imports ## peewee_async related imports
import peewee import peewee
from peewee_async import Manager, PostgresqlDatabase from peewee import Model, BaseModel
from peewee_async import Manager, PostgresqlDatabase, execute
from functools import partial
# we instantiate a custom loop so we can pass it to our db manager # we instantiate a custom loop so we can pass it to our db manager
## from peewee_async docs: ## from peewee_async docs:
@ -19,42 +21,77 @@ from peewee_async import Manager, PostgresqlDatabase
# with manager! Its all automatic. But you can run Manager.connect() or # with manager! Its all automatic. But you can run Manager.connect() or
# Manager.close() when you need it. # Manager.close() when you need it.
class AsyncManager(Manager):
"""Inherit the peewee_async manager with our own object
configuration
# let's create a simple key value store: database.allow_sync = False
class KeyValue(peewee.Model): """
key = peewee.CharField(max_length=40, unique=True)
text = peewee.TextField(default='') def __init__(self, _model_class, *args, **kwargs):
super(AsyncManager, self).__init__(*args, **kwargs)
self._model_class = _model_class
self.database.allow_sync = False
def _do_fill(self, method, *args, **kwargs):
_class_method = getattr(super(AsyncManager, self), method)
pf = partial(_class_method, self._model_class)
return pf(*args, **kwargs)
def new(self, *args, **kwargs):
return self._do_fill('create', *args, **kwargs)
def get(self, *args, **kwargs):
return self._do_fill('get', *args, **kwargs)
def execute(self, query):
return execute(query)
def _get_meta_db_class(db):
"""creating a declartive class model for db"""
class _BlockedMeta(BaseModel):
def __new__(cls, name, bases, attrs):
_instance = super(_BlockedMeta, cls).__new__(cls, name, bases, attrs)
_instance.objects = AsyncManager(_instance, db)
return _instance
class _Base(Model, metaclass=_BlockedMeta):
def to_dict(self):
return self._data
class Meta: class Meta:
database = database database=db
return _Base
# create table synchronously
KeyValue.create_table(True)
# OPTIONAL: close synchronous connection
database.close()
# OPTIONAL: disable any future syncronous calls
objects.database.allow_sync = False # this will raise AssertionError on ANY sync call
app = Sanic('peewee_example') def declarative_base(*args, **kwargs):
"""Returns a new Modeled Class after inheriting meta and Model classes"""
db = PostgresqlDatabase(*args, **kwargs)
return _get_meta_db_class(db)
@app.listener('before_server_start')
def setup(app, loop): AsyncBaseModel = declarative_base(database='test',
database = PostgresqlDatabase(database='test',
host='127.0.0.1', host='127.0.0.1',
user='postgres', user='postgres',
password='mysecretpassword') password='mysecretpassword')
objects = Manager(database, loop=loop) # let's create a simple key value store:
class KeyValue(AsyncBaseModel):
key = peewee.CharField(max_length=40, unique=True)
text = peewee.TextField(default='')
app = Sanic('peewee_example')
@app.route('/post/<key>/<value>') @app.route('/post/<key>/<value>')
async def post(request, key, value): async def post(request, key, value):
""" """
Save get parameters to database Save get parameters to database
""" """
obj = await objects.create(KeyValue, key=key, text=value) obj = await KeyValue.objects.new(key=key, text=value)
return json({'object_id': obj.id}) return json({'object_id': obj.id})
@ -63,7 +100,7 @@ async def get(request):
""" """
Load all objects from database Load all objects from database
""" """
all_objects = await objects.execute(KeyValue.select()) all_objects = await KeyValue.objects.execute(KeyValue.select())
serialized_obj = [] serialized_obj = []
for obj in all_objects: for obj in all_objects:
serialized_obj.append({ serialized_obj.append({

View File

@ -9,4 +9,5 @@ async def test(request):
return json({"test": True}) return json({"test": True})
app.run(host="0.0.0.0", port=8000) if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@ -70,6 +70,11 @@ def query_string(request):
# Run Server # Run Server
# ----------------------------------------------- # # ----------------------------------------------- #
@app.listener('before_server_start')
def before_start(app, loop):
log.info("SERVER STARTING")
@app.listener('after_server_start') @app.listener('after_server_start')
def after_start(app, loop): def after_start(app, loop):
log.info("OH OH OH OH OHHHHHHHH") log.info("OH OH OH OH OHHHHHHHH")
@ -77,7 +82,13 @@ def after_start(app, loop):
@app.listener('before_server_stop') @app.listener('before_server_stop')
def before_stop(app, loop): def before_stop(app, loop):
log.info("SERVER STOPPING")
@app.listener('after_server_stop')
def after_stop(app, loop):
log.info("TRIED EVERYTHING") log.info("TRIED EVERYTHING")
app.run(host="0.0.0.0", port=8000, debug=True) if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@ -1,7 +1,7 @@
import logging import logging
import re import re
import warnings import warnings
from asyncio import get_event_loop from asyncio import get_event_loop, ensure_future, CancelledError
from collections import deque, defaultdict from collections import deque, defaultdict
from functools import partial from functools import partial
from inspect import isawaitable, stack, getmodulename from inspect import isawaitable, stack, getmodulename
@ -54,6 +54,7 @@ class Sanic:
self.listeners = defaultdict(list) self.listeners = defaultdict(list)
self.is_running = False self.is_running = False
self.websocket_enabled = False self.websocket_enabled = False
self.websocket_tasks = []
# Register alternative method names # Register alternative method names
self.go_fast = self.run self.go_fast = self.run
@ -101,7 +102,8 @@ class Sanic:
return decorator return decorator
# Decorator # Decorator
def route(self, uri, methods=frozenset({'GET'}), host=None): def route(self, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False):
"""Decorate a function to be registered as a route """Decorate a function to be registered as a route
:param uri: path of the URL :param uri: path of the URL
@ -117,34 +119,42 @@ class Sanic:
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) host=host, strict_slashes=strict_slashes)
return handler return handler
return response return response
# Shorthand method decorators # Shorthand method decorators
def get(self, uri, host=None): def get(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"GET"}), host=host) return self.route(uri, methods=frozenset({"GET"}), host=host,
strict_slashes=strict_slashes)
def post(self, uri, host=None): def post(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"POST"}), host=host) return self.route(uri, methods=frozenset({"POST"}), host=host,
strict_slashes=strict_slashes)
def put(self, uri, host=None): def put(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"PUT"}), host=host) return self.route(uri, methods=frozenset({"PUT"}), host=host,
strict_slashes=strict_slashes)
def head(self, uri, host=None): def head(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"HEAD"}), host=host) return self.route(uri, methods=frozenset({"HEAD"}), host=host,
strict_slashes=strict_slashes)
def options(self, uri, host=None): def options(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host) return self.route(uri, methods=frozenset({"OPTIONS"}), host=host,
strict_slashes=strict_slashes)
def patch(self, uri, host=None): def patch(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"PATCH"}), host=host) return self.route(uri, methods=frozenset({"PATCH"}), host=host,
strict_slashes=strict_slashes)
def delete(self, uri, host=None): def delete(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"DELETE"}), host=host) return self.route(uri, methods=frozenset({"DELETE"}), host=host,
strict_slashes=strict_slashes)
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None): def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False):
"""A helper method to register class instance or """A helper method to register class instance or
functions as a handler to the application url functions as a handler to the application url
routes. routes.
@ -168,17 +178,18 @@ class Sanic:
if isinstance(handler, CompositionView): if isinstance(handler, CompositionView):
methods = handler.handlers.keys() methods = handler.handlers.keys()
self.route(uri=uri, methods=methods, host=host)(handler) self.route(uri=uri, methods=methods, host=host,
strict_slashes=strict_slashes)(handler)
return handler return handler
# Decorator # Decorator
def websocket(self, uri, host=None): def websocket(self, uri, host=None, strict_slashes=False):
"""Decorate a function to be registered as a websocket route """Decorate a function to be registered as a websocket route
:param uri: path of the URL :param uri: path of the URL
:param host: :param host:
:return: decorated function :return: decorated function
""" """
self.websocket_enabled = True self.enable_websocket()
# Fix case where the user did not prefix the URL with a / # Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working # and will probably get confused as to why it's not working
@ -190,22 +201,31 @@ class Sanic:
request.app = self request.app = self
protocol = request.transport.get_protocol() protocol = request.transport.get_protocol()
ws = await protocol.websocket_handshake(request) ws = await protocol.websocket_handshake(request)
# schedule the application handler
# its future is kept in self.websocket_tasks in case it
# needs to be cancelled due to the server being stopped
fut = ensure_future(handler(request, ws, *args, **kwargs))
self.websocket_tasks.append(fut)
try: try:
# invoke the application handler await fut
await handler(request, ws, *args, **kwargs) except (CancelledError, ConnectionClosed):
except ConnectionClosed:
pass pass
self.websocket_tasks.remove(fut)
await ws.close() await ws.close()
self.router.add(uri=uri, handler=websocket_handler, self.router.add(uri=uri, handler=websocket_handler,
methods=frozenset({'GET'}), host=host) methods=frozenset({'GET'}), host=host,
strict_slashes=strict_slashes)
return handler return handler
return response return response
def add_websocket_route(self, handler, uri, host=None): def add_websocket_route(self, handler, uri, host=None,
strict_slashes=False):
"""A helper method to register a function as a websocket route.""" """A helper method to register a function as a websocket route."""
return self.websocket(uri, host=host)(handler) return self.websocket(uri, host=host,
strict_slashes=strict_slashes)(handler)
def enable_websocket(self, enable=True): def enable_websocket(self, enable=True):
"""Enable or disable the support for websocket. """Enable or disable the support for websocket.
@ -213,6 +233,14 @@ class Sanic:
Websocket is enabled automatically if websocket routes are Websocket is enabled automatically if websocket routes are
added to the application. added to the application.
""" """
if not self.websocket_enabled:
# if the server is stopped, we want to cancel any ongoing
# websocket tasks, to allow the server to exit promptly
@self.listener('before_server_stop')
def cancel_websocket_tasks(app, loop):
for task in self.websocket_tasks:
task.cancel()
self.websocket_enabled = enable self.websocket_enabled = enable
def remove_route(self, uri, clean_cache=True, host=None): def remove_route(self, uri, clean_cache=True, host=None):
@ -305,7 +333,7 @@ class Sanic:
the output URL's query string. the output URL's query string.
:param view_name: string referencing the view name :param view_name: string referencing the view name
:param **kwargs: keys and values that are used to build request :param \*\*kwargs: keys and values that are used to build request
parameters and query string arguments. parameters and query string arguments.
:return: the built URL :return: the built URL
@ -550,6 +578,10 @@ class Sanic:
"""This kills the Sanic""" """This kills the Sanic"""
get_event_loop().stop() get_event_loop().stop()
def __call__(self):
"""gunicorn compatibility"""
return self
async def create_server(self, host="127.0.0.1", port=8000, debug=False, async def create_server(self, host="127.0.0.1", port=8000, debug=False,
before_start=None, after_start=None, before_start=None, after_start=None,
before_stop=None, after_stop=None, ssl=None, before_stop=None, after_stop=None, ssl=None,
@ -658,6 +690,7 @@ class Sanic:
server_settings['run_async'] = True server_settings['run_async'] = True
# Serve # Serve
if host and port:
proto = "http" proto = "http"
if ssl is not None: if ssl is not None:
proto = "https" proto = "https"

View File

@ -19,7 +19,7 @@ _Translator.update({
def _quote(str): def _quote(str):
r"""Quote a string for use in a cookie header. """Quote a string for use in a cookie header.
If the string does not need to be double-quoted, then just return the If the string does not need to be double-quoted, then just return the
string. Otherwise, surround the string in doublequotes and quote string. Otherwise, surround the string in doublequotes and quote
(with a \) special characters. (with a \) special characters.

View File

@ -251,8 +251,7 @@ def text(body, status=200, headers=None,
:param body: Response data to be encoded. :param body: Response data to be encoded.
:param status: Response code. :param status: Response code.
:param headers: Custom Headers. :param headers: Custom Headers.
:param content_type: :param content_type: the content type (string) of the response
the content type (string) of the response
""" """
return HTTPResponse( return HTTPResponse(
body, status=status, headers=headers, body, status=status, headers=headers,
@ -266,8 +265,7 @@ def raw(body, status=200, headers=None,
:param body: Response data. :param body: Response data.
:param status: Response code. :param status: Response code.
:param headers: Custom Headers. :param headers: Custom Headers.
:param content_type: :param content_type: the content type (string) of the response.
the content type (string) of the response
""" """
return HTTPResponse(body_bytes=body, status=status, headers=headers, return HTTPResponse(body_bytes=body, status=status, headers=headers,
content_type=content_type) content_type=content_type)
@ -316,9 +314,9 @@ def stream(
content_type="text/plain; charset=utf-8"): content_type="text/plain; charset=utf-8"):
"""Accepts an coroutine `streaming_fn` which can be used to """Accepts an coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `StreamingHTTPResponse`. write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
Example usage:
``` Example usage::
@app.route("/") @app.route("/")
async def index(request): async def index(request):
async def streaming_fn(response): async def streaming_fn(response):
@ -326,7 +324,6 @@ def stream(
await response.write('bar') await response.write('bar')
return stream(streaming_fn, content_type='text/plain') return stream(streaming_fn, content_type='text/plain')
```
:param streaming_fn: A coroutine accepts a response and :param streaming_fn: A coroutine accepts a response and
writes content to that response. writes content to that response.

View File

@ -75,8 +75,9 @@ class Router:
"""Parse a parameter string into its constituent name, type, and """Parse a parameter string into its constituent name, type, and
pattern pattern
For example: For example::
`parse_parameter_string('<param_one:[A-z]>')` ->
parse_parameter_string('<param_one:[A-z]>')` ->
('param_one', str, '[A-z]') ('param_one', str, '[A-z]')
:param parameter_string: String to parse :param parameter_string: String to parse
@ -95,9 +96,15 @@ class Router:
return name, _type, pattern return name, _type, pattern
def add(self, uri, methods, handler, host=None): def add(self, uri, methods, handler, host=None, strict_slashes=False):
# add regular version # add regular version
self._add(uri, methods, handler, host) self._add(uri, methods, handler, host)
if strict_slashes:
return
# Add versions with and without trailing /
slash_is_missing = ( slash_is_missing = (
not uri[-1] == '/' not uri[-1] == '/'
and not self.routes_all.get(uri + '/', False) and not self.routes_all.get(uri + '/', False)

View File

@ -313,7 +313,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
after_start=None, before_stop=None, after_stop=None, debug=False, after_start=None, before_stop=None, after_stop=None, debug=False,
request_timeout=60, ssl=None, sock=None, request_max_size=None, request_timeout=60, ssl=None, sock=None, request_max_size=None,
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
register_sys_signals=True, run_async=False): register_sys_signals=True, run_async=False, connections=None,
signal=Signal()):
"""Start asynchronous HTTP Server on an individual process. """Start asynchronous HTTP Server on an individual process.
:param host: Address to host on :param host: Address to host on
@ -349,8 +350,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
trigger_events(before_start, loop) trigger_events(before_start, loop)
connections = set() connections = connections if connections is not None else set()
signal = Signal()
server = partial( server = partial(
protocol, protocol,
loop=loop, loop=loop,

166
sanic/worker.py Normal file
View File

@ -0,0 +1,166 @@
import os
import sys
import signal
import asyncio
import logging
try:
import ssl
except ImportError:
ssl = None
import uvloop
import gunicorn.workers.base as base
from sanic.server import trigger_events, serve, HttpProtocol, Signal
from sanic.websocket import WebSocketProtocol
class GunicornWorker(base.Worker):
def __init__(self, *args, **kw): # pragma: no cover
super().__init__(*args, **kw)
cfg = self.cfg
if cfg.is_ssl:
self.ssl_context = self._create_ssl_context(cfg)
else:
self.ssl_context = None
self.servers = []
self.connections = set()
self.exit_code = 0
self.signal = Signal()
def init_process(self):
# create new event_loop after fork
asyncio.get_event_loop().close()
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
super().init_process()
def run(self):
is_debug = self.log.loglevel == logging.DEBUG
protocol = (WebSocketProtocol if self.app.callable.websocket_enabled
else HttpProtocol)
self._server_settings = self.app.callable._helper(
host=None,
port=None,
loop=self.loop,
debug=is_debug,
protocol=protocol,
ssl=self.ssl_context,
run_async=True
)
self._server_settings.pop('sock')
trigger_events(self._server_settings.get('before_start', []),
self.loop)
self._server_settings['before_start'] = ()
self._runner = asyncio.ensure_future(self._run(), loop=self.loop)
try:
self.loop.run_until_complete(self._runner)
self.app.callable.is_running = True
trigger_events(self._server_settings.get('after_start', []),
self.loop)
self.loop.run_until_complete(self._check_alive())
trigger_events(self._server_settings.get('before_stop', []),
self.loop)
self.loop.run_until_complete(self.close())
finally:
trigger_events(self._server_settings.get('after_stop', []),
self.loop)
self.loop.close()
sys.exit(self.exit_code)
async def close(self):
if self.servers:
# stop accepting connections
self.log.info("Stopping server: %s, connections: %s",
self.pid, len(self.connections))
for server in self.servers:
server.close()
await server.wait_closed()
self.servers.clear()
# prepare connections for closing
self.signal.stopped = True
for conn in self.connections:
conn.close_if_idle()
while self.connections:
await asyncio.sleep(0.1)
async def _run(self):
for sock in self.sockets:
self.servers.append(await serve(
sock=sock,
connections=self.connections,
signal=self.signal,
**self._server_settings
))
async def _check_alive(self):
# If our parent changed then we shut down.
pid = os.getpid()
try:
while self.alive:
self.notify()
if pid == os.getpid() and self.ppid != os.getppid():
self.alive = False
self.log.info("Parent changed, shutting down: %s", self)
else:
await asyncio.sleep(1.0, loop=self.loop)
except (Exception, BaseException, GeneratorExit, KeyboardInterrupt):
pass
@staticmethod
def _create_ssl_context(cfg):
""" Creates SSLContext instance for usage in asyncio.create_server.
See ssl.SSLSocket.__init__ for more details.
"""
ctx = ssl.SSLContext(cfg.ssl_version)
ctx.load_cert_chain(cfg.certfile, cfg.keyfile)
ctx.verify_mode = cfg.cert_reqs
if cfg.ca_certs:
ctx.load_verify_locations(cfg.ca_certs)
if cfg.ciphers:
ctx.set_ciphers(cfg.ciphers)
return ctx
def init_signals(self):
# Set up signals through the event loop API.
self.loop.add_signal_handler(signal.SIGQUIT, self.handle_quit,
signal.SIGQUIT, None)
self.loop.add_signal_handler(signal.SIGTERM, self.handle_exit,
signal.SIGTERM, None)
self.loop.add_signal_handler(signal.SIGINT, self.handle_quit,
signal.SIGINT, None)
self.loop.add_signal_handler(signal.SIGWINCH, self.handle_winch,
signal.SIGWINCH, None)
self.loop.add_signal_handler(signal.SIGUSR1, self.handle_usr1,
signal.SIGUSR1, None)
self.loop.add_signal_handler(signal.SIGABRT, self.handle_abort,
signal.SIGABRT, None)
# Don't let SIGTERM and SIGUSR1 disturb active requests
# by interrupting system calls
signal.siginterrupt(signal.SIGTERM, False)
signal.siginterrupt(signal.SIGUSR1, False)
def handle_quit(self, sig, frame):
self.alive = False
self.cfg.worker_int(self)
def handle_abort(self, sig, frame):
self.alive = False
self.exit_code = 1
self.cfg.worker_abort(self)

View File

@ -23,6 +23,29 @@ def test_shorthand_routes_get():
request, response = app.test_client.post('/get') request, response = app.test_client.post('/get')
assert response.status == 405 assert response.status == 405
def test_route_strict_slash():
app = Sanic('test_route_strict_slash')
@app.get('/get', strict_slashes=True)
def handler(request):
return text('OK')
@app.post('/post/', strict_slashes=True)
def handler(request):
return text('OK')
request, response = app.test_client.get('/get')
assert response.text == 'OK'
request, response = app.test_client.get('/get/')
assert response.status == 404
request, response = app.test_client.post('/post/')
assert response.text == 'OK'
request, response = app.test_client.post('/post')
assert response.status == 404
def test_route_optional_slash(): def test_route_optional_slash():
app = Sanic('test_route_optional_slash') app = Sanic('test_route_optional_slash')

View File

@ -15,13 +15,13 @@ def test_methods(method):
class DummyView(HTTPMethodView): class DummyView(HTTPMethodView):
def get(self, request): async def get(self, request):
return text('', headers={'method': 'GET'}) return text('', headers={'method': 'GET'})
def post(self, request): def post(self, request):
return text('', headers={'method': 'POST'}) return text('', headers={'method': 'POST'})
def put(self, request): async def put(self, request):
return text('', headers={'method': 'PUT'}) return text('', headers={'method': 'PUT'})
def head(self, request): def head(self, request):
@ -30,7 +30,7 @@ def test_methods(method):
def options(self, request): def options(self, request):
return text('', headers={'method': 'OPTIONS'}) return text('', headers={'method': 'OPTIONS'})
def patch(self, request): async def patch(self, request):
return text('', headers={'method': 'PATCH'}) return text('', headers={'method': 'PATCH'})
def delete(self, request): def delete(self, request):