diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..ee8ca2be
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,6 @@
+FROM python:3.6
+
+ADD . /app
+WORKDIR /app
+
+RUN pip install tox
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..ad64412f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,4 @@
+test:
+ find . -name "*.pyc" -delete
+ docker build -t sanic/test-image .
+ docker run -t sanic/test-image tox
diff --git a/README.rst b/README.rst
index 3f565f71..878ba24c 100644
--- a/README.rst
+++ b/README.rst
@@ -59,6 +59,13 @@ Installation
- ``python -m pip install sanic``
+To install sanic without uvloop or json using bash, you can provide either or both of these environmental variables
+using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the NO_X to true will stop that features
+installation.
+
+- ``SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true python -m pip install sanic``
+
+
Documentation
-------------
diff --git a/docs/conf.py b/docs/conf.py
index 21b9b9cf..c97f3c19 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -22,7 +22,7 @@ import sanic
# -- General configuration ------------------------------------------------
-extensions = []
+extensions = ['sphinx.ext.autodoc']
templates_path = ['_templates']
@@ -68,7 +68,6 @@ pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
-
# -- Options for HTML output ----------------------------------------------
# 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".
html_static_path = ['_static']
-
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'Sanicdoc'
-
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
@@ -110,21 +107,14 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
-latex_documents = [
- (master_doc, 'Sanic.tex', 'Sanic Documentation',
- 'Sanic contributors', 'manual'),
-]
-
+latex_documents = [(master_doc, 'Sanic.tex', 'Sanic Documentation',
+ 'Sanic contributors', 'manual'), ]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
-man_pages = [
- (master_doc, 'sanic', 'Sanic Documentation',
- [author], 1)
-]
-
+man_pages = [(master_doc, 'sanic', 'Sanic Documentation', [author], 1)]
# -- Options for Texinfo output -------------------------------------------
@@ -132,13 +122,10 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
- (master_doc, 'Sanic', 'Sanic Documentation',
- author, 'Sanic', 'One line description of project.',
- 'Miscellaneous'),
+ (master_doc, 'Sanic', 'Sanic Documentation', author, 'Sanic',
+ 'One line description of project.', 'Miscellaneous'),
]
-
-
# -- Options for Epub output ----------------------------------------------
# Bibliographic Dublin Core info.
@@ -150,8 +137,6 @@ epub_copyright = copyright
# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']
-
-
# -- Custom Settings -------------------------------------------------------
suppress_warnings = ['image.nonlocal_uri']
diff --git a/docs/index.rst b/docs/index.rst
index 43cd0ba2..3fa63d5c 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -16,6 +16,7 @@ Guides
sanic/blueprints
sanic/config
sanic/cookies
+ sanic/streaming
sanic/class_based_views
sanic/custom_protocol
sanic/ssl
diff --git a/docs/sanic/blueprints.md b/docs/sanic/blueprints.md
index d1338023..5fe20e54 100644
--- a/docs/sanic/blueprints.md
+++ b/docs/sanic/blueprints.md
@@ -57,7 +57,7 @@ Blueprints have much the same functionality as an application instance.
### WebSocket routes
-WebSocket handlers can be registered on a blueprint using the `@bp.route`
+WebSocket handlers can be registered on a blueprint using the `@bp.websocket`
decorator or `bp.add_websocket_route` method.
### Middleware
@@ -66,7 +66,7 @@ Using blueprints allows you to also register middleware globally.
```python
@bp.middleware
-async def halt_request(request):
+async def print_on_request(request):
print("I am a spy")
@bp.middleware('request')
@@ -116,7 +116,7 @@ bp = Blueprint('my_blueprint')
async def setup_connection(app, loop):
global database
database = mysql.connect(host='127.0.0.1'...)
-
+
@bp.listener('after_server_stop')
async def close_connection(app, loop):
await database.close()
@@ -142,7 +142,7 @@ blueprint_v2 = Blueprint('v2', url_prefix='/v2')
@blueprint_v1.route('/')
async def api_v1_root(request):
return text('Welcome to version 1 of our documentation')
-
+
@blueprint_v2.route('/')
async def api_v2_root(request):
return text('Welcome to version 2 of our documentation')
diff --git a/docs/sanic/class_based_views.md b/docs/sanic/class_based_views.md
index 02b02140..ace8bf9c 100644
--- a/docs/sanic/class_based_views.md
+++ b/docs/sanic/class_based_views.md
@@ -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
If you need any URL parameters, as discussed in the routing guide, include them
@@ -128,4 +146,4 @@ view.add(['POST', 'PUT'], lambda request: text('I am a post/put method'))
app.add_route(view, '/')
```
-Note: currently you cannot build a URL for a CompositionView using `url_for`.
+Note: currently you cannot build a URL for a CompositionView using `url_for`.
diff --git a/docs/config.md b/docs/sanic/config.md
similarity index 79%
rename from docs/config.md
rename to docs/sanic/config.md
index f5d56467..3ed40fda 100644
--- a/docs/config.md
+++ b/docs/sanic/config.md
@@ -29,6 +29,14 @@ In general the convention is to only have UPPERCASE configuration parameters. Th
There are several ways how to load configuration.
+### From environment variables.
+
+Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically. You can pass the `load_vars` boolean to the Sanic constructor to override that:
+
+```python
+app = Sanic(load_vars=False)
+```
+
### From an Object
If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module:
@@ -71,8 +79,7 @@ DB_USER = 'appuser'
Out of the box there are just a few predefined values which can be overwritten when creating the application.
-| Variable | Default | Description |
-| ----------------- | --------- | --------------------------------- |
-| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
-| REQUEST_TIMEOUT | 60 | How long a request can take (sec) |
-
+ | Variable | Default | Description |
+ | ----------------- | --------- | --------------------------------- |
+ | REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
+ | REQUEST_TIMEOUT | 60 | How long a request can take (sec) |
diff --git a/docs/sanic/cookies.md b/docs/sanic/cookies.md
index 0a1042a2..e71bcc47 100644
--- a/docs/sanic/cookies.md
+++ b/docs/sanic/cookies.md
@@ -5,7 +5,7 @@ both read and write cookies, which are stored as key-value pairs.
## Reading cookies
-A user's cookies can be accessed `Request` object's `cookie` dictionary.
+A user's cookies can be accessed via the `Request` object's `cookies` dictionary.
```python
from sanic.response import text
@@ -42,20 +42,20 @@ from sanic.response import text
@app.route("/cookie")
async def test(request):
response = text("Time to eat some cookies muahaha")
-
+
# This cookie will be set to expire in 0 seconds
del response.cookies['kill_me']
-
+
# This cookie will self destruct in 5 seconds
response.cookies['short_life'] = 'Glad to be here'
response.cookies['short_life']['max-age'] = 5
del response.cookies['favorite_color']
-
+
# This cookie will remain unchanged
response.cookies['favorite_color'] = 'blue'
response.cookies['favorite_color'] = 'pink'
del response.cookies['favorite_color']
-
+
return response
```
diff --git a/docs/sanic/decorators.md b/docs/sanic/decorators.md
new file mode 100644
index 00000000..db2d369b
--- /dev/null
+++ b/docs/sanic/decorators.md
@@ -0,0 +1,39 @@
+# Handler Decorators
+
+Since Sanic handlers are simple Python functions, you can apply decorators to them in a similar manner to Flask. A typical use case is when you want some code to run before a handler's code is executed.
+
+## Authorization Decorator
+
+Let's say you want to check that a user is authorized to access a particular endpoint. You can create a decorator that wraps a handler function, checks a request if the client is authorized to access a resource, and sends the appropriate response.
+
+
+```python
+from functools import wraps
+from sanic.response import json
+
+def authorized():
+ def decorator(f):
+ @wraps(f)
+ async def decorated_function(request, *args, **kwargs):
+ # run some method that checks the request
+ # for the client's authorization status
+ is_authorized = check_request_for_authorization_status(request)
+
+ if is_authorized:
+ # the user is authorized.
+ # run the handler method and return the response
+ response = await f(request, *args, **kwargs)
+ return response
+ else:
+ # the user is not authorized.
+ return json({'status': 'not_authorized'}, 403)
+ return decorated_function
+ return decorator
+
+
+@app.route("/")
+@authorized()
+async def test(request):
+ return json({status: 'authorized'})
+```
+
diff --git a/docs/sanic/deploying.md b/docs/sanic/deploying.md
index d5f3ad06..cc89759b 100644
--- a/docs/sanic/deploying.md
+++ b/docs/sanic/deploying.md
@@ -44,3 +44,15 @@ directly run by the interpreter.
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337, workers=4)
```
+
+## Running via Gunicorn
+
+[Gunicorn](http://gunicorn.org/) ‘Green Unicorn’ is a WSGI HTTP Server for UNIX.
+It’s a pre-fork worker model ported from Ruby’s Unicorn project.
+
+In order to run Sanic application with Gunicorn, you need to use the special `sanic.worker.GunicornWorker`
+for Gunicorn `worker-class` argument:
+
+```
+gunicorn --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker
+```
diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md
index ac08c36e..ec22a9d2 100644
--- a/docs/sanic/extensions.md
+++ b/docs/sanic/extensions.md
@@ -16,3 +16,7 @@ A list of Sanic extensions created by the community.
- [Sanic EnvConfig](https://github.com/jamesstidard/sanic-envconfig): Pull environment variables into your sanic config.
- [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the
`Babel` library
+- [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.
+- [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose.
+- [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic
diff --git a/docs/sanic/index.rst b/docs/sanic/index.rst
index eb9eb286..08797e9f 100644
--- a/docs/sanic/index.rst
+++ b/docs/sanic/index.rst
@@ -7,8 +7,8 @@ On top of being Flask-like, Sanic supports async request handlers. This means y
Sanic is developed `on GitHub `_. Contributions are welcome!
-Sanic aspires to be simple:
--------------------
+Sanic aspires to be simple
+---------------------------
.. code:: python
diff --git a/docs/sanic/middleware.md b/docs/sanic/middleware.md
index 58ff8feb..b2e8b45a 100644
--- a/docs/sanic/middleware.md
+++ b/docs/sanic/middleware.md
@@ -1,9 +1,13 @@
-# Middleware
+# Middleware And Listeners
Middleware are functions which are executed before or after requests to the
server. They can be used to modify the *request to* or *response from*
user-defined handler functions.
+Additionally, Sanic providers listeners which allow you to run code at various points of your application's lifecycle.
+
+## Middleware
+
There are two types of middleware: request and response. Both are declared
using the `@app.middleware` decorator, with the decorator's parameter being a
string representing its type: `'request'` or `'response'`. Response middleware
@@ -64,3 +68,45 @@ async def halt_request(request):
async def halt_response(request, response):
return text('I halted the response')
```
+
+## Listeners
+
+If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners:
+
+- `before_server_start`
+- `after_server_start`
+- `before_server_stop`
+- `after_server_stop`
+
+These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.
+
+For example:
+
+```python
+@app.listener('before_server_start')
+async def setup_db(app, loop):
+ app.db = await db_setup()
+
+@app.listener('after_server_start')
+async def notify_server_started(app, loop):
+ print('Server successfully started!')
+
+@app.listener('before_server_stop')
+async def notify_server_stopping(app, loop):
+ print('Server shutting down!')
+
+@app.listener('after_server_stop')
+async def close_db(app, loop):
+ await app.db.close()
+```
+
+If you want to schedule a background task to run after the loop has started,
+Sanic provides the `add_task` method to easily do so.
+
+```python
+async def notify_server_started_after_five_seconds():
+ await asyncio.sleep(5)
+ print('Server successfully started!')
+
+app.add_task(notify_server_started_after_five_seconds())
+```
diff --git a/docs/sanic/request_data.md b/docs/sanic/request_data.md
index a86a0f21..87f619a3 100644
--- a/docs/sanic/request_data.md
+++ b/docs/sanic/request_data.md
@@ -9,30 +9,34 @@ The following variables are accessible as properties on `Request` objects:
```python
from sanic.response import json
-
+
@app.route("/json")
def post_json(request):
return json({ "received": True, "message": request.json })
```
-
+
- `args` (dict) - Query string variables. A query string is the section of a
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
- the `args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
+ the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`.
The request's `query_string` variable holds the unparsed string value.
```python
from sanic.response import json
-
+
@app.route("/query_string")
def query_string(request):
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
```
+- `raw_args` (dict) - On many cases you would need to access the url arguments in
+ a less packed dictionary. For same previous URL `?key1=value1&key2=value2`, the
+ `raw_args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
+
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
```python
from sanic.response import json
-
+
@app.route("/files")
def post_json(request):
test_file = request.files.get('test')
@@ -50,7 +54,7 @@ The following variables are accessible as properties on `Request` objects:
```python
from sanic.response import json
-
+
@app.route("/form")
def post_json(request):
return json({ "received": True, "form_data": request.form, "test": request.form.get('test') })
@@ -58,15 +62,15 @@ The following variables are accessible as properties on `Request` objects:
- `body` (bytes) - Posted raw body. This property allows retrieval of the
request's raw data, regardless of content type.
-
+
```python
from sanic.response import text
-
+
@app.route("/users", methods=["POST",])
def create_user(request):
return text("You are trying to create a user with the following POST: %s" % request.body)
```
-
+
- `ip` (str) - IP address of the requester.
- `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object.
diff --git a/docs/sanic/response.md b/docs/sanic/response.md
index 627fbc7e..12718ca1 100644
--- a/docs/sanic/response.md
+++ b/docs/sanic/response.md
@@ -2,7 +2,7 @@
Use functions in `sanic.response` module to create responses.
-- `text` - Plain text response
+## Plain Text
```python
from sanic import response
@@ -13,7 +13,7 @@ def handle_request(request):
return response.text('Hello world!')
```
-- `html` - HTML response
+## HTML
```python
from sanic import response
@@ -24,7 +24,7 @@ def handle_request(request):
return response.html('
Hello world!
')
```
-- `json` - JSON response
+## JSON
```python
@@ -36,7 +36,7 @@ def handle_request(request):
return response.json({'message': 'Hello world!'})
```
-- `file` - File response
+## File
```python
from sanic import response
@@ -47,7 +47,7 @@ async def handle_request(request):
return await response.file('/srv/www/whatever.png')
```
-- `stream` - Streaming response
+## Streaming
```python
from sanic import response
@@ -55,12 +55,12 @@ from sanic import response
@app.route("/streaming")
async def index(request):
async def streaming_fn(response):
- await response.write('foo')
- await response.write('bar')
+ response.write('foo')
+ response.write('bar')
return response.stream(streaming_fn, content_type='text/plain')
```
-- `redirect` - Redirect response
+## Redirect
```python
from sanic import response
@@ -71,7 +71,9 @@ def handle_request(request):
return response.redirect('/json')
```
-- `raw` - Raw response, response without encoding the body
+## Raw
+
+Response without encoding the body
```python
from sanic import response
@@ -82,6 +84,7 @@ def handle_request(request):
return response.raw('raw data')
```
+## Modify headers or status
To modify headers or status code, pass the `headers` or `status` argument to those functions:
diff --git a/environment.yml b/environment.yml
index 7eee43a5..298ea552 100644
--- a/environment.yml
+++ b/environment.yml
@@ -15,4 +15,5 @@ dependencies:
- httptools>=0.0.9
- ujson>=1.35
- aiofiles>=0.3.0
+ - websockets>=3.2
- https://github.com/channelcat/docutils-fork/zipball/master
\ No newline at end of file
diff --git a/examples/asyncorm/__init__.py b/examples/asyncorm/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/asyncorm/__main__.py b/examples/asyncorm/__main__.py
new file mode 100644
index 00000000..20537f7b
--- /dev/null
+++ b/examples/asyncorm/__main__.py
@@ -0,0 +1,140 @@
+from sanic import Sanic
+from sanic.exceptions import NotFound
+from sanic.response import json
+from sanic.views import HTTPMethodView
+
+from asyncorm import configure_orm
+from asyncorm.exceptions import QuerysetError
+
+from library.models import Book
+from library.serializer import BookSerializer
+
+app = Sanic(name=__name__)
+
+
+@app.listener('before_server_start')
+def orm_configure(sanic, loop):
+ db_config = {'database': 'sanic_example',
+ 'host': 'localhost',
+ 'user': 'sanicdbuser',
+ 'password': 'sanicDbPass',
+ }
+
+ # configure_orm needs a dictionary with:
+ # * the database configuration
+ # * the application/s where the models are defined
+ orm_app = configure_orm({'loop': loop, # always use the sanic loop!
+ 'db_config': db_config,
+ 'modules': ['library', ], # list of apps
+ })
+
+ # orm_app is the object that orchestrates the whole ORM
+ # sync_db should be run only once, better do that as external command
+ # it creates the tables in the database!!!!
+ # orm_app.sync_db()
+
+
+# for all the 404 lets handle the exceptions
+@app.exception(NotFound)
+def ignore_404s(request, exception):
+ return json({'method': request.method,
+ 'status': exception.status_code,
+ 'error': exception.args[0],
+ 'results': None,
+ })
+
+
+# now the propper sanic workflow
+class BooksView(HTTPMethodView):
+ def arg_parser(self, request):
+ parsed_args = {}
+ for k, v in request.args.items():
+ parsed_args[k] = v[0]
+ return parsed_args
+
+ async def get(self, request):
+ filtered_by = self.arg_parser(request)
+
+ if filtered_by:
+ q_books = await Book.objects.filter(**filtered_by)
+ else:
+ q_books = await Book.objects.all()
+
+ books = [BookSerializer.serialize(book) for book in q_books]
+
+ return json({'method': request.method,
+ 'status': 200,
+ 'results': books or None,
+ 'count': len(books),
+ })
+
+ async def post(self, request):
+ # populate the book with the data in the request
+ book = Book(**request.json)
+
+ # and await on save
+ await book.save()
+
+ return json({'method': request.method,
+ 'status': 201,
+ 'results': BookSerializer.serialize(book),
+ })
+
+
+class BookView(HTTPMethodView):
+ async def get_object(self, request, book_id):
+ try:
+ # await on database consults
+ book = await Book.objects.get(**{'id': book_id})
+ except QuerysetError as e:
+ raise NotFound(e.args[0])
+ return book
+
+ async def get(self, request, book_id):
+ # await on database consults
+ book = await self.get_object(request, book_id)
+
+ return json({'method': request.method,
+ 'status': 200,
+ 'results': BookSerializer.serialize(book),
+ })
+
+ async def put(self, request, book_id):
+ # await on database consults
+ book = await self.get_object(request, book_id)
+ # await on save
+ await book.save(**request.json)
+
+ return json({'method': request.method,
+ 'status': 200,
+ 'results': BookSerializer.serialize(book),
+ })
+
+ async def patch(self, request, book_id):
+ # await on database consults
+ book = await self.get_object(request, book_id)
+ # await on save
+ await book.save(**request.json)
+
+ return json({'method': request.method,
+ 'status': 200,
+ 'results': BookSerializer.serialize(book),
+ })
+
+ async def delete(self, request, book_id):
+ # await on database consults
+ book = await self.get_object(request, book_id)
+ # await on its deletion
+ await book.delete()
+
+ return json({'method': request.method,
+ 'status': 200,
+ 'results': None
+ })
+
+
+app.add_route(BooksView.as_view(), '/books/')
+app.add_route(BookView.as_view(), '/books//')
+
+if __name__ == '__main__':
+ app.run()
diff --git a/examples/asyncorm/library/__init__.py b/examples/asyncorm/library/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/asyncorm/library/models.py b/examples/asyncorm/library/models.py
new file mode 100644
index 00000000..51cacb1b
--- /dev/null
+++ b/examples/asyncorm/library/models.py
@@ -0,0 +1,21 @@
+from asyncorm.model import Model
+from asyncorm.fields import CharField, IntegerField, DateField
+
+
+BOOK_CHOICES = (
+ ('hard cover', 'hard cover book'),
+ ('paperback', 'paperback book')
+)
+
+
+# This is a simple model definition
+class Book(Model):
+ name = CharField(max_length=50)
+ synopsis = CharField(max_length=255)
+ book_type = CharField(max_length=15, null=True, choices=BOOK_CHOICES)
+ pages = IntegerField(null=True)
+ date_created = DateField(auto_now=True)
+
+ class Meta():
+ ordering = ['name', ]
+ unique_together = ['name', 'synopsis']
diff --git a/examples/asyncorm/library/serializer.py b/examples/asyncorm/library/serializer.py
new file mode 100644
index 00000000..00faa91e
--- /dev/null
+++ b/examples/asyncorm/library/serializer.py
@@ -0,0 +1,15 @@
+from asyncorm.model import ModelSerializer, SerializerMethod
+from library.models import Book
+
+
+class BookSerializer(ModelSerializer):
+ book_type = SerializerMethod()
+
+ def get_book_type(self, instance):
+ return instance.book_type_display()
+
+ class Meta():
+ model = Book
+ fields = [
+ 'id', 'name', 'synopsis', 'book_type', 'pages', 'date_created'
+ ]
diff --git a/examples/asyncorm/requirements.txt b/examples/asyncorm/requirements.txt
new file mode 100644
index 00000000..9b824ce6
--- /dev/null
+++ b/examples/asyncorm/requirements.txt
@@ -0,0 +1,2 @@
+asyncorm==0.0.7
+sanic==0.4.1
diff --git a/examples/detailed_example.py b/examples/detailed_example.py
new file mode 100644
index 00000000..99e71cb1
--- /dev/null
+++ b/examples/detailed_example.py
@@ -0,0 +1,136 @@
+# This demo requires aioredis and environmental variables established in ENV_VARS
+import json
+import logging
+import os
+
+from datetime import datetime
+
+import aioredis
+
+import sanic
+from sanic import Sanic
+
+
+ENV_VARS = ["REDIS_HOST", "REDIS_PORT",
+ "REDIS_MINPOOL", "REDIS_MAXPOOL",
+ "REDIS_PASS", "APP_LOGFILE"]
+
+app = Sanic(name=__name__)
+
+logger = None
+
+
+@app.middleware("request")
+async def log_uri(request):
+ # Simple middleware to log the URI endpoint that was called
+ logger.info("URI called: {0}".format(request.url))
+
+
+@app.listener('before_server_start')
+async def before_server_start(app, loop):
+ logger.info("Starting redis pool")
+ app.redis_pool = await aioredis.create_pool(
+ (app.config.REDIS_HOST, int(app.config.REDIS_PORT)),
+ minsize=int(app.config.REDIS_MINPOOL),
+ maxsize=int(app.config.REDIS_MAXPOOL),
+ password=app.config.REDIS_PASS)
+
+
+@app.listener('after_server_stop')
+async def after_server_stop(app, loop):
+ logger.info("Closing redis pool")
+ app.redis_pool.close()
+ await app.redis_pool.wait_closed()
+
+
+@app.middleware("request")
+async def attach_db_connectors(request):
+ # Just put the db objects in the request for easier access
+ logger.info("Passing redis pool to request object")
+ request["redis"] = request.app.redis_pool
+
+
+@app.route("/state/", methods=["GET"])
+async def access_state(request, user_id):
+ try:
+ # Check to see if the value is in cache, if so lets return that
+ with await request["redis"] as redis_conn:
+ state = await redis_conn.get(user_id, encoding="utf-8")
+ if state:
+ return sanic.response.json({"msg": "Success",
+ "status": 200,
+ "success": True,
+ "data": json.loads(state),
+ "finished_at": datetime.now().isoformat()})
+ # Then state object is not in redis
+ logger.critical("Unable to find user_data in cache.")
+ return sanic.response.HTTPResponse({"msg": "User state not found",
+ "success": False,
+ "status": 404,
+ "finished_at": datetime.now().isoformat()}, status=404)
+ except aioredis.ProtocolError:
+ logger.critical("Unable to connect to state cache")
+ return sanic.response.HTTPResponse({"msg": "Internal Server Error",
+ "status": 500,
+ "success": False,
+ "finished_at": datetime.now().isoformat()}, status=500)
+
+
+@app.route("/state//push", methods=["POST"])
+async def set_state(request, user_id):
+ try:
+ # Pull a connection from the pool
+ with await request["redis"] as redis_conn:
+ # Set the value in cache to your new value
+ await redis_conn.set(user_id, json.dumps(request.json), expire=1800)
+ logger.info("Successfully pushed state to cache")
+ return sanic.response.HTTPResponse({"msg": "Successfully pushed state to cache",
+ "success": True,
+ "status": 200,
+ "finished_at": datetime.now().isoformat()})
+ except aioredis.ProtocolError:
+ logger.critical("Unable to connect to state cache")
+ return sanic.response.HTTPResponse({"msg": "Internal Server Error",
+ "status": 500,
+ "success": False,
+ "finished_at": datetime.now().isoformat()}, status=500)
+
+
+def configure():
+ # Setup environment variables
+ env_vars = [os.environ.get(v, None) for v in ENV_VARS]
+ if not all(env_vars):
+ # Send back environment variables that were not set
+ return False, ", ".join([ENV_VARS[i] for i, flag in env_vars if not flag])
+ else:
+ # Add all the env vars to our app config
+ app.config.update({k: v for k, v in zip(ENV_VARS, env_vars)})
+ setup_logging()
+ return True, None
+
+
+def setup_logging():
+ logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
+ logging_format += "%(module)s::%(funcName)s():l%(lineno)d: "
+ logging_format += "%(message)s"
+
+ logging.basicConfig(
+ filename=app.config.APP_LOGFILE,
+ format=logging_format,
+ level=logging.DEBUG)
+
+
+def main(result, missing):
+ if result:
+ try:
+ app.run(host="0.0.0.0", port=8080, debug=True)
+ except:
+ logging.critical("User killed server. Closing")
+ else:
+ logging.critical("Unable to start. Missing environment variables [{0}]".format(missing))
+
+
+if __name__ == "__main__":
+ result, missing = configure()
+ logger = logging.getLogger()
+ main(result, missing)
diff --git a/examples/sanic_aiomysql_with_global_pool.py b/examples/sanic_aiomysql_with_global_pool.py
new file mode 100644
index 00000000..65d5832d
--- /dev/null
+++ b/examples/sanic_aiomysql_with_global_pool.py
@@ -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)
diff --git a/examples/sanic_aioredis_example.py b/examples/sanic_aioredis_example.py
new file mode 100644
index 00000000..8ba51617
--- /dev/null
+++ b/examples/sanic_aioredis_example.py
@@ -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)
diff --git a/examples/sanic_peewee.py b/examples/sanic_peewee.py
index 8db8ddff..aaa139f1 100644
--- a/examples/sanic_peewee.py
+++ b/examples/sanic_peewee.py
@@ -1,3 +1,4 @@
+
## You need the following additional packages for this example
# aiopg
# peewee_async
@@ -10,8 +11,9 @@ from sanic.response import json
## peewee_async related imports
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
## from peewee_async docs:
@@ -19,42 +21,77 @@ from peewee_async import Manager, PostgresqlDatabase
# with manager! It’s all automatic. But you can run Manager.connect() or
# 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:
-class KeyValue(peewee.Model):
- key = peewee.CharField(max_length=40, unique=True)
- text = peewee.TextField(default='')
+ database.allow_sync = False
+ """
- class Meta:
- database = database
+ def __init__(self, _model_class, *args, **kwargs):
+ super(AsyncManager, self).__init__(*args, **kwargs)
+ self._model_class = _model_class
+ self.database.allow_sync = False
-# create table synchronously
-KeyValue.create_table(True)
+ 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)
-# OPTIONAL: close synchronous connection
-database.close()
+ def new(self, *args, **kwargs):
+ return self._do_fill('create', *args, **kwargs)
-# OPTIONAL: disable any future syncronous calls
-objects.database.allow_sync = False # this will raise AssertionError on ANY sync call
+ def get(self, *args, **kwargs):
+ return self._do_fill('get', *args, **kwargs)
+
+ def execute(self, query):
+ return execute(query)
-app = Sanic('peewee_example')
+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
-@app.listener('before_server_start')
-def setup(app, loop):
- database = PostgresqlDatabase(database='test',
+ class _Base(Model, metaclass=_BlockedMeta):
+
+ def to_dict(self):
+ return self._data
+
+ class Meta:
+ database=db
+ return _Base
+
+
+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)
+
+
+AsyncBaseModel = declarative_base(database='test',
host='127.0.0.1',
user='postgres',
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//')
async def post(request, key, value):
"""
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})
@@ -63,7 +100,7 @@ async def get(request):
"""
Load all objects from database
"""
- all_objects = await objects.execute(KeyValue.select())
+ all_objects = await KeyValue.objects.execute(KeyValue.select())
serialized_obj = []
for obj in all_objects:
serialized_obj.append({
diff --git a/examples/simple_server.py b/examples/simple_server.py
index 24e3570f..a803feb8 100644
--- a/examples/simple_server.py
+++ b/examples/simple_server.py
@@ -9,4 +9,5 @@ async def test(request):
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)
diff --git a/examples/try_everything.py b/examples/try_everything.py
index f7191ecc..da3cc515 100644
--- a/examples/try_everything.py
+++ b/examples/try_everything.py
@@ -70,6 +70,11 @@ def query_string(request):
# Run Server
# ----------------------------------------------- #
+@app.listener('before_server_start')
+def before_start(app, loop):
+ log.info("SERVER STARTING")
+
+
@app.listener('after_server_start')
def after_start(app, loop):
log.info("OH OH OH OH OHHHHHHHH")
@@ -77,7 +82,13 @@ def after_start(app, loop):
@app.listener('before_server_stop')
def before_stop(app, loop):
+ log.info("SERVER STOPPING")
+
+
+@app.listener('after_server_stop')
+def after_stop(app, loop):
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)
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 1f11a90c..28014eb6 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,18 +1,10 @@
-aiocache
aiofiles
-aiohttp
+aiohttp==1.3.5
beautifulsoup4
-bottle
coverage
-falcon
-gunicorn
httptools
-kyoukai
+flake8
pytest
-recommonmark
-sphinx
-sphinx_rtd_theme
-tornado
tox
ujson
uvloop
diff --git a/sanic/app.py b/sanic/app.py
index 65a6196f..ef9ac57c 100644
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -1,7 +1,7 @@
import logging
import re
import warnings
-from asyncio import get_event_loop
+from asyncio import get_event_loop, ensure_future, CancelledError
from collections import deque, defaultdict
from functools import partial
from inspect import isawaitable, stack, getmodulename
@@ -25,7 +25,8 @@ from sanic.websocket import WebSocketProtocol, ConnectionClosed
class Sanic:
- def __init__(self, name=None, router=None, error_handler=None):
+ def __init__(self, name=None, router=None, error_handler=None,
+ load_env=True):
# Only set up a default log handler if the
# end-user application didn't set anything up.
if not logging.root.handlers and log.level == logging.NOTSET:
@@ -44,7 +45,7 @@ class Sanic:
self.name = name
self.router = router or Router()
self.error_handler = error_handler or ErrorHandler()
- self.config = Config()
+ self.config = Config(load_env=load_env)
self.request_middleware = deque()
self.response_middleware = deque()
self.blueprints = {}
@@ -54,6 +55,7 @@ class Sanic:
self.listeners = defaultdict(list)
self.is_running = False
self.websocket_enabled = False
+ self.websocket_tasks = []
# Register alternative method names
self.go_fast = self.run
@@ -101,7 +103,8 @@ class Sanic:
return 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
:param uri: path of the URL
@@ -117,34 +120,42 @@ class Sanic:
def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler,
- host=host)
+ host=host, strict_slashes=strict_slashes)
return handler
return response
# Shorthand method decorators
- def get(self, uri, host=None):
- return self.route(uri, methods=frozenset({"GET"}), host=host)
+ def get(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=frozenset({"GET"}), host=host,
+ strict_slashes=strict_slashes)
- def post(self, uri, host=None):
- return self.route(uri, methods=frozenset({"POST"}), host=host)
+ def post(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=frozenset({"POST"}), host=host,
+ strict_slashes=strict_slashes)
- def put(self, uri, host=None):
- return self.route(uri, methods=frozenset({"PUT"}), host=host)
+ def put(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=frozenset({"PUT"}), host=host,
+ strict_slashes=strict_slashes)
- def head(self, uri, host=None):
- return self.route(uri, methods=frozenset({"HEAD"}), host=host)
+ def head(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=frozenset({"HEAD"}), host=host,
+ strict_slashes=strict_slashes)
- def options(self, uri, host=None):
- return self.route(uri, methods=frozenset({"OPTIONS"}), host=host)
+ def options(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=frozenset({"OPTIONS"}), host=host,
+ strict_slashes=strict_slashes)
- def patch(self, uri, host=None):
- return self.route(uri, methods=frozenset({"PATCH"}), host=host)
+ def patch(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=frozenset({"PATCH"}), host=host,
+ strict_slashes=strict_slashes)
- def delete(self, uri, host=None):
- return self.route(uri, methods=frozenset({"DELETE"}), host=host)
+ def delete(self, uri, host=None, strict_slashes=False):
+ 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
functions as a handler to the application url
routes.
@@ -168,17 +179,18 @@ class Sanic:
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
- self.route(uri=uri, methods=methods, host=host)(handler)
+ self.route(uri=uri, methods=methods, host=host,
+ strict_slashes=strict_slashes)(handler)
return handler
# 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
:param uri: path of the URL
:param host:
:return: decorated function
"""
- self.websocket_enabled = True
+ self.enable_websocket()
# Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working
@@ -190,22 +202,31 @@ class Sanic:
request.app = self
protocol = request.transport.get_protocol()
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:
- # invoke the application handler
- await handler(request, ws, *args, **kwargs)
- except ConnectionClosed:
+ await fut
+ except (CancelledError, ConnectionClosed):
pass
+ self.websocket_tasks.remove(fut)
await ws.close()
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 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."""
- return self.websocket(uri, host=host)(handler)
+ return self.websocket(uri, host=host,
+ strict_slashes=strict_slashes)(handler)
def enable_websocket(self, enable=True):
"""Enable or disable the support for websocket.
@@ -213,6 +234,14 @@ class Sanic:
Websocket is enabled automatically if websocket routes are
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
def remove_route(self, uri, clean_cache=True, host=None):
@@ -305,7 +334,7 @@ class Sanic:
the output URL's query string.
: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.
:return: the built URL
@@ -526,19 +555,24 @@ class Sanic:
if protocol is None:
protocol = (WebSocketProtocol if self.websocket_enabled
else HttpProtocol)
+ if stop_event is not None:
+ if debug:
+ warnings.simplefilter('default')
+ warnings.warn("stop_event will be removed from future versions.",
+ DeprecationWarning)
server_settings = self._helper(
host=host, port=port, debug=debug, before_start=before_start,
after_start=after_start, before_stop=before_stop,
after_stop=after_stop, ssl=ssl, sock=sock, workers=workers,
loop=loop, protocol=protocol, backlog=backlog,
- stop_event=stop_event, register_sys_signals=register_sys_signals)
+ register_sys_signals=register_sys_signals)
try:
self.is_running = True
if workers == 1:
serve(**server_settings)
else:
- serve_multiple(server_settings, workers, stop_event)
+ serve_multiple(server_settings, workers)
except:
log.exception(
'Experienced exception while trying to serve')
@@ -550,6 +584,10 @@ class Sanic:
"""This kills the Sanic"""
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,
before_start=None, after_start=None,
before_stop=None, after_stop=None, ssl=None,
@@ -563,13 +601,17 @@ class Sanic:
if protocol is None:
protocol = (WebSocketProtocol if self.websocket_enabled
else HttpProtocol)
+ if stop_event is not None:
+ if debug:
+ warnings.simplefilter('default')
+ warnings.warn("stop_event will be removed from future versions.",
+ DeprecationWarning)
server_settings = self._helper(
host=host, port=port, debug=debug, before_start=before_start,
after_start=after_start, before_stop=before_stop,
after_stop=after_stop, ssl=ssl, sock=sock,
loop=loop or get_event_loop(), protocol=protocol,
- backlog=backlog, stop_event=stop_event,
- run_async=True)
+ backlog=backlog, run_async=True)
return await serve(**server_settings)
@@ -589,7 +631,11 @@ class Sanic:
context = create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(cert, keyfile=key)
ssl = context
-
+ if stop_event is not None:
+ if debug:
+ warnings.simplefilter('default')
+ warnings.warn("stop_event will be removed from future versions.",
+ DeprecationWarning)
if loop is not None:
if debug:
warnings.simplefilter('default')
@@ -658,9 +704,10 @@ class Sanic:
server_settings['run_async'] = True
# Serve
- proto = "http"
- if ssl is not None:
- proto = "https"
- log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
+ if host and port:
+ proto = "http"
+ if ssl is not None:
+ proto = "https"
+ log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
return server_settings
diff --git a/sanic/blueprints.py b/sanic/blueprints.py
index e17d4b81..7e9953e0 100644
--- a/sanic/blueprints.py
+++ b/sanic/blueprints.py
@@ -3,7 +3,9 @@ from collections import defaultdict, namedtuple
from sanic.constants import HTTP_METHODS
from sanic.views import CompositionView
-FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host'])
+FutureRoute = namedtuple('Route',
+ ['handler', 'uri', 'methods',
+ 'host', 'strict_slashes'])
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
@@ -44,7 +46,8 @@ class Blueprint:
app.route(
uri=uri[1:] if uri.startswith('//') else uri,
methods=future.methods,
- host=future.host or self.host
+ host=future.host or self.host,
+ strict_slashes=future.strict_slashes
)(future.handler)
for future in self.websocket_routes:
@@ -55,7 +58,8 @@ class Blueprint:
uri = url_prefix + future.uri if url_prefix else future.uri
app.websocket(
uri=uri,
- host=future.host or self.host
+ host=future.host or self.host,
+ strict_slashes=future.strict_slashes
)(future.handler)
# Middleware
@@ -82,19 +86,21 @@ class Blueprint:
for listener in listeners:
app.listener(event)(listener)
- def route(self, uri, methods=frozenset({'GET'}), host=None):
+ def route(self, uri, methods=frozenset({'GET'}), host=None,
+ strict_slashes=False):
"""Create a blueprint route from a decorated function.
:param uri: endpoint at which the route will be accessible.
:param methods: list of acceptable HTTP methods.
"""
def decorator(handler):
- route = FutureRoute(handler, uri, methods, host)
+ route = FutureRoute(handler, uri, methods, host, strict_slashes)
self.routes.append(route)
return handler
return decorator
- 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):
"""Create a blueprint route from a function.
:param handler: function for handling uri requests. Accepts function,
@@ -115,16 +121,17 @@ class Blueprint:
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
- self.route(uri=uri, methods=methods, host=host)(handler)
+ self.route(uri=uri, methods=methods, host=host,
+ strict_slashes=strict_slashes)(handler)
return handler
- def websocket(self, uri, host=None):
+ def websocket(self, uri, host=None, strict_slashes=False):
"""Create a blueprint websocket route from a decorated function.
:param uri: endpoint at which the route will be accessible.
"""
def decorator(handler):
- route = FutureRoute(handler, uri, [], host)
+ route = FutureRoute(handler, uri, [], host, strict_slashes)
self.websocket_routes.append(route)
return handler
return decorator
@@ -183,23 +190,30 @@ class Blueprint:
self.statics.append(static)
# Shorthand method decorators
- def get(self, uri, host=None):
- return self.route(uri, methods=["GET"], host=host)
+ def get(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=["GET"], host=host,
+ strict_slashes=strict_slashes)
- def post(self, uri, host=None):
- return self.route(uri, methods=["POST"], host=host)
+ def post(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=["POST"], host=host,
+ strict_slashes=strict_slashes)
- def put(self, uri, host=None):
- return self.route(uri, methods=["PUT"], host=host)
+ def put(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=["PUT"], host=host,
+ strict_slashes=strict_slashes)
- def head(self, uri, host=None):
- return self.route(uri, methods=["HEAD"], host=host)
+ def head(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=["HEAD"], host=host,
+ strict_slashes=strict_slashes)
- def options(self, uri, host=None):
- return self.route(uri, methods=["OPTIONS"], host=host)
+ def options(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=["OPTIONS"], host=host,
+ strict_slashes=strict_slashes)
- def patch(self, uri, host=None):
- return self.route(uri, methods=["PATCH"], host=host)
+ def patch(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=["PATCH"], host=host,
+ strict_slashes=strict_slashes)
- def delete(self, uri, host=None):
- return self.route(uri, methods=["DELETE"], host=host)
+ def delete(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=["DELETE"], host=host,
+ strict_slashes=strict_slashes)
diff --git a/sanic/config.py b/sanic/config.py
index 3b9a102a..406c44e6 100644
--- a/sanic/config.py
+++ b/sanic/config.py
@@ -1,9 +1,12 @@
import os
+
import types
+SANIC_PREFIX = 'SANIC_'
+
class Config(dict):
- def __init__(self, defaults=None):
+ def __init__(self, defaults=None, load_env=True):
super().__init__(defaults or {})
self.LOGO = """
▄▄▄▄▄
@@ -29,6 +32,9 @@ class Config(dict):
self.REQUEST_MAX_SIZE = 100000000 # 100 megababies
self.REQUEST_TIMEOUT = 60 # 60 seconds
+ if load_env:
+ self.load_environment_vars()
+
def __getattr__(self, attr):
try:
return self[attr]
@@ -90,3 +96,13 @@ class Config(dict):
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)
+
+ def load_environment_vars(self):
+ for k, v in os.environ.items():
+ """
+ Looks for any SANIC_ prefixed environment variables and applies
+ them to the configuration if present.
+ """
+ if k.startswith(SANIC_PREFIX):
+ _, config_key = k.split(SANIC_PREFIX, 1)
+ self[config_key] = v
diff --git a/sanic/cookies.py b/sanic/cookies.py
index ae77bb44..ce096cd2 100644
--- a/sanic/cookies.py
+++ b/sanic/cookies.py
@@ -19,7 +19,7 @@ _Translator.update({
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
string. Otherwise, surround the string in doublequotes and quote
(with a \) special characters.
diff --git a/sanic/request.py b/sanic/request.py
index 68743c79..4a15c22f 100644
--- a/sanic/request.py
+++ b/sanic/request.py
@@ -121,6 +121,10 @@ class Request(dict):
self.parsed_args = RequestParameters()
return self.parsed_args
+ @property
+ def raw_args(self):
+ return {k: v[0] for k, v in self.args.items()}
+
@property
def cookies(self):
if self._cookies is None:
@@ -142,10 +146,16 @@ class Request(dict):
@property
def scheme(self):
- if self.transport.get_extra_info('sslcontext'):
- return 'https'
+ if self.app.websocket_enabled \
+ and self.headers.get('upgrade') == 'websocket':
+ scheme = 'ws'
+ else:
+ scheme = 'http'
- return 'http'
+ if self.transport.get_extra_info('sslcontext'):
+ scheme += 's'
+
+ return scheme
@property
def host(self):
diff --git a/sanic/response.py b/sanic/response.py
index eb2d3e49..4eecaf79 100644
--- a/sanic/response.py
+++ b/sanic/response.py
@@ -1,5 +1,6 @@
from mimetypes import guess_type
from os import path
+
try:
from ujson import dumps as json_dumps
except:
@@ -132,8 +133,8 @@ class StreamingHTTPResponse(BaseHTTPResponse):
async def stream(
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
- """Streams headers, runs the `streaming_fn` callback that writes content
- to the response body, then finalizes the response body.
+ """Streams headers, runs the `streaming_fn` callback that writes
+ content to the response body, then finalizes the response body.
"""
headers = self.get_headers(
version, keep_alive=keep_alive,
@@ -167,12 +168,12 @@ class StreamingHTTPResponse(BaseHTTPResponse):
return (b'HTTP/%b %d %b\r\n'
b'%b'
b'%b\r\n') % (
- version.encode(),
- self.status,
- status,
- timeout_header,
- headers
- )
+ version.encode(),
+ self.status,
+ status,
+ timeout_header,
+ headers
+ )
class HTTPResponse(BaseHTTPResponse):
@@ -216,14 +217,14 @@ class HTTPResponse(BaseHTTPResponse):
b'%b'
b'%b\r\n'
b'%b') % (
- version.encode(),
- self.status,
- status,
- b'keep-alive' if keep_alive else b'close',
- timeout_header,
- headers,
- self.body
- )
+ version.encode(),
+ self.status,
+ status,
+ b'keep-alive' if keep_alive else b'close',
+ timeout_header,
+ headers,
+ self.body
+ )
@property
def cookies(self):
@@ -251,8 +252,7 @@ def text(body, status=200, headers=None,
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
- :param content_type:
- the content type (string) of the response
+ :param content_type: the content type (string) of the response
"""
return HTTPResponse(
body, status=status, headers=headers,
@@ -266,8 +266,7 @@ def raw(body, status=200, headers=None,
:param body: Response data.
:param status: Response code.
:param headers: Custom Headers.
- :param content_type:
- the content type (string) of the response
+ :param content_type: the content type (string) of the response.
"""
return HTTPResponse(body_bytes=body, status=status, headers=headers,
content_type=content_type)
@@ -316,17 +315,16 @@ def stream(
content_type="text/plain; charset=utf-8"):
"""Accepts an coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
- Example usage:
- ```
- @app.route("/")
- async def index(request):
- async def streaming_fn(response):
- await response.write('foo')
- await response.write('bar')
+ Example usage::
- return stream(streaming_fn, content_type='text/plain')
- ```
+ @app.route("/")
+ async def index(request):
+ async def streaming_fn(response):
+ await response.write('foo')
+ await response.write('bar')
+
+ return stream(streaming_fn, content_type='text/plain')
:param streaming_fn: A coroutine accepts a response and
writes content to that response.
@@ -334,7 +332,11 @@ def stream(
:param headers: Custom Headers.
"""
return StreamingHTTPResponse(
- streaming_fn, headers=headers, content_type=content_type, status=200)
+ streaming_fn,
+ headers=headers,
+ content_type=content_type,
+ status=status
+ )
def redirect(to, headers=None, status=302,
diff --git a/sanic/router.py b/sanic/router.py
index 38b1c029..f7877f15 100644
--- a/sanic/router.py
+++ b/sanic/router.py
@@ -75,9 +75,10 @@ class Router:
"""Parse a parameter string into its constituent name, type, and
pattern
- For example:
- `parse_parameter_string('')` ->
- ('param_one', str, '[A-z]')
+ For example::
+
+ parse_parameter_string('')` ->
+ ('param_one', str, '[A-z]')
:param parameter_string: String to parse
:return: tuple containing
@@ -95,9 +96,15 @@ class Router:
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
self._add(uri, methods, handler, host)
+
+ if strict_slashes:
+ return
+
+ # Add versions with and without trailing /
slash_is_missing = (
not uri[-1] == '/'
and not self.routes_all.get(uri + '/', False)
diff --git a/sanic/server.py b/sanic/server.py
index 39816e28..14ce1ffd 100644
--- a/sanic/server.py
+++ b/sanic/server.py
@@ -4,10 +4,13 @@ import traceback
import warnings
from functools import partial
from inspect import isawaitable
-from multiprocessing import Process, Event
+from multiprocessing import Process
from os import set_inheritable
-from signal import SIGTERM, SIGINT
-from signal import signal as signal_func
+from signal import (
+ SIGTERM, SIGINT,
+ signal as signal_func,
+ Signals
+)
from socket import socket, SOL_SOCKET, SO_REUSEADDR
from time import time
@@ -313,7 +316,8 @@ 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, ssl=None, sock=None, request_max_size=None,
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.
:param host: Address to host on
@@ -329,7 +333,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
`app` instance and `loop`
:param after_stop: function to be executed when a stop signal is
received after it is respected. Takes arguments
- `app` instance and `loop`
+ `app` instance and `loop`
:param debug: enables debug output (slows server)
:param request_timeout: time in seconds
:param ssl: SSLContext
@@ -349,8 +353,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
trigger_events(before_start, loop)
- connections = set()
- signal = Signal()
+ connections = connections if connections is not None else set()
server = partial(
protocol,
loop=loop,
@@ -421,7 +424,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
loop.close()
-def serve_multiple(server_settings, workers, stop_event=None):
+def serve_multiple(server_settings, workers):
"""Start multiple server processes simultaneously. Stop on interrupt
and terminate signals, and drain connections when complete.
@@ -448,11 +451,12 @@ def serve_multiple(server_settings, workers, stop_event=None):
server_settings['host'] = None
server_settings['port'] = None
- if stop_event is None:
- stop_event = Event()
+ def sig_handler(signal, frame):
+ log.info("Received signal {}. Shutting down.".format(
+ Signals(signal).name))
- signal_func(SIGINT, lambda s, f: stop_event.set())
- signal_func(SIGTERM, lambda s, f: stop_event.set())
+ signal_func(SIGINT, lambda s, f: sig_handler(s, f))
+ signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
processes = []
for _ in range(workers):
diff --git a/sanic/testing.py b/sanic/testing.py
index 4fde428c..1cad6a7b 100644
--- a/sanic/testing.py
+++ b/sanic/testing.py
@@ -22,7 +22,10 @@ class SanicTestClient:
cookies=cookies, connector=conn) as session:
async with getattr(
session, method.lower())(url, *args, **kwargs) as response:
- response.text = await response.text()
+ try:
+ response.text = await response.text()
+ except UnicodeDecodeError as e:
+ response.text = None
response.body = await response.read()
return response
diff --git a/sanic/worker.py b/sanic/worker.py
new file mode 100644
index 00000000..7a8303d8
--- /dev/null
+++ b/sanic/worker.py
@@ -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)
diff --git a/setup.py b/setup.py
index 594c88b9..deb52c27 100644
--- a/setup.py
+++ b/setup.py
@@ -4,8 +4,10 @@ Sanic
import codecs
import os
import re
-from setuptools import setup
+from distutils.errors import DistutilsPlatformError
+from distutils.util import strtobool
+from setuptools import setup
with codecs.open(os.path.join(os.path.abspath(os.path.dirname(
__file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp:
@@ -15,7 +17,7 @@ with codecs.open(os.path.join(os.path.abspath(os.path.dirname(
except IndexError:
raise RuntimeError('Unable to determine version.')
-setup_kwargs = {
+setup_kwargs = {
'name': 'sanic',
'version': version,
'url': 'http://github.com/channelcat/sanic/',
@@ -35,23 +37,32 @@ setup_kwargs = {
],
}
+ujson = 'ujson>=1.35'
+uvloop = 'uvloop>=0.5.3'
+
+requirements = [
+ 'httptools>=0.0.9',
+ uvloop,
+ ujson,
+ 'aiofiles>=0.3.0',
+ 'websockets>=3.2',
+]
+if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
+ print("Installing without uJSON")
+ requirements.remove(ujson)
+
+if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")):
+ print("Installing without uvLoop")
+ requirements.remove(uvloop)
+
try:
- normal_requirements = [
- 'httptools>=0.0.9',
- 'uvloop>=0.5.3',
- 'ujson>=1.35',
- 'aiofiles>=0.3.0',
- 'websockets>=3.2',
- ]
- setup_kwargs['install_requires'] = normal_requirements
+ setup_kwargs['install_requires'] = requirements
setup(**setup_kwargs)
except DistutilsPlatformError as exception:
- windows_requirements = [
- 'httptools>=0.0.9',
- 'aiofiles>=0.3.0',
- 'websockets>=3.2',
- ]
- setup_kwargs['install_requires'] = windows_requirements
+ requirements.remove(ujson)
+ requirements.remove(uvloop)
+ print("Installing without uJSON or uvLoop")
+ setup_kwargs['install_requires'] = requirements
setup(**setup_kwargs)
# Installation was successful
diff --git a/tests/static/python.png b/tests/static/python.png
new file mode 100644
index 00000000..52fda109
Binary files /dev/null and b/tests/static/python.png differ
diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py
index fed4a03a..46726836 100644
--- a/tests/test_blueprints.py
+++ b/tests/test_blueprints.py
@@ -24,6 +24,33 @@ def test_bp():
assert response.text == 'Hello'
+def test_bp_strict_slash():
+ app = Sanic('test_route_strict_slash')
+ bp = Blueprint('test_text')
+
+ @bp.get('/get', strict_slashes=True)
+ def handler(request):
+ return text('OK')
+
+ @bp.post('/post/', strict_slashes=True)
+ def handler(request):
+ return text('OK')
+
+ app.blueprint(bp)
+
+ 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_bp_with_url_prefix():
app = Sanic('test_text')
bp = Blueprint('test_text', url_prefix='/test1')
diff --git a/tests/test_config.py b/tests/test_config.py
index c7e41ade..aa7a0e4d 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -16,6 +16,17 @@ def test_load_from_object():
assert app.config.CONFIG_VALUE == 'should be used'
assert 'not_for_config' not in app.config
+def test_auto_load_env():
+ environ["SANIC_TEST_ANSWER"] = "42"
+ app = Sanic()
+ assert app.config.TEST_ANSWER == "42"
+ del environ["SANIC_TEST_ANSWER"]
+
+def test_auto_load_env():
+ environ["SANIC_TEST_ANSWER"] = "42"
+ app = Sanic(load_env=False)
+ assert getattr(app.config, 'TEST_ANSWER', None) == None
+ del environ["SANIC_TEST_ANSWER"]
def test_load_from_file():
app = Sanic('test_load_from_file')
diff --git a/tests/test_payload_too_large.py b/tests/test_payload_too_large.py
index a1a58d3d..ecac605c 100644
--- a/tests/test_payload_too_large.py
+++ b/tests/test_payload_too_large.py
@@ -1,49 +1,47 @@
from sanic import Sanic
-from sanic.response import text
from sanic.exceptions import PayloadTooLarge
-
-data_received_app = Sanic('data_received')
-data_received_app.config.REQUEST_MAX_SIZE = 1
-data_received_default_app = Sanic('data_received_default')
-data_received_default_app.config.REQUEST_MAX_SIZE = 1
-on_header_default_app = Sanic('on_header')
-on_header_default_app.config.REQUEST_MAX_SIZE = 500
-
-
-@data_received_app.route('/1')
-async def handler1(request):
- return text('OK')
-
-
-@data_received_app.exception(PayloadTooLarge)
-def handler_exception(request, exception):
- return text('Payload Too Large from error_handler.', 413)
+from sanic.response import text
def test_payload_too_large_from_error_handler():
+ data_received_app = Sanic('data_received')
+ data_received_app.config.REQUEST_MAX_SIZE = 1
+
+ @data_received_app.route('/1')
+ async def handler1(request):
+ return text('OK')
+
+ @data_received_app.exception(PayloadTooLarge)
+ def handler_exception(request, exception):
+ return text('Payload Too Large from error_handler.', 413)
+
response = data_received_app.test_client.get('/1', gather_request=False)
assert response.status == 413
assert response.text == 'Payload Too Large from error_handler.'
-@data_received_default_app.route('/1')
-async def handler2(request):
- return text('OK')
-
-
def test_payload_too_large_at_data_received_default():
+ data_received_default_app = Sanic('data_received_default')
+ data_received_default_app.config.REQUEST_MAX_SIZE = 1
+
+ @data_received_default_app.route('/1')
+ async def handler2(request):
+ return text('OK')
+
response = data_received_default_app.test_client.get(
'/1', gather_request=False)
assert response.status == 413
assert response.text == 'Error: Payload Too Large'
-@on_header_default_app.route('/1')
-async def handler3(request):
- return text('OK')
-
-
def test_payload_too_large_at_on_header_default():
+ on_header_default_app = Sanic('on_header')
+ on_header_default_app.config.REQUEST_MAX_SIZE = 500
+
+ @on_header_default_app.post('/1')
+ async def handler3(request):
+ return text('OK')
+
data = 'a' * 1000
response = on_header_default_app.test_client.post(
'/1', gather_request=False, data=data)
diff --git a/tests/test_redirect.py b/tests/test_redirect.py
index 421ee1cf..f5b734e3 100644
--- a/tests/test_redirect.py
+++ b/tests/test_redirect.py
@@ -88,4 +88,7 @@ def test_chained_redirect(redirect_app):
assert request.url.endswith('/1')
assert response.status == 200
assert response.text == 'OK'
- assert response.url.endswith('/3')
+ try:
+ assert response.url.endswith('/3')
+ except AttributeError:
+ assert response.url.path.endswith('/3')
diff --git a/tests/test_routes.py b/tests/test_routes.py
index afefe4a7..3506db66 100644
--- a/tests/test_routes.py
+++ b/tests/test_routes.py
@@ -23,6 +23,29 @@ def test_shorthand_routes_get():
request, response = app.test_client.post('/get')
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():
app = Sanic('test_route_optional_slash')
diff --git a/tests/test_static.py b/tests/test_static.py
index 783a56c7..091d63a4 100644
--- a/tests/test_static.py
+++ b/tests/test_static.py
@@ -25,7 +25,7 @@ def get_file_content(static_file_directory, file_name):
return file.read()
-@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
+@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
def test_static_file(static_file_directory, file_name):
app = Sanic('test_static')
app.static(
diff --git a/tests/test_views.py b/tests/test_views.py
index 627a3b6c..40fc1adf 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -15,13 +15,13 @@ def test_methods(method):
class DummyView(HTTPMethodView):
- def get(self, request):
+ async def get(self, request):
return text('', headers={'method': 'GET'})
def post(self, request):
return text('', headers={'method': 'POST'})
- def put(self, request):
+ async def put(self, request):
return text('', headers={'method': 'PUT'})
def head(self, request):
@@ -30,7 +30,7 @@ def test_methods(method):
def options(self, request):
return text('', headers={'method': 'OPTIONS'})
- def patch(self, request):
+ async def patch(self, request):
return text('', headers={'method': 'PATCH'})
def delete(self, request):
diff --git a/tox.ini b/tox.ini
index 33e4298f..0e6dc7c6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,12 +10,7 @@ python =
[testenv]
deps =
- aiofiles
- aiohttp
- websockets
- pytest
- beautifulsoup4
- coverage
+ -rrequirements-dev.txt
commands =
pytest tests {posargs}