Merge branch 'master' into add_register_sys_signals_flag

This commit is contained in:
Eli Uriegas 2017-01-21 10:25:57 -06:00 committed by GitHub
commit 214162adf0
52 changed files with 2350 additions and 469 deletions

6
.gitignore vendored
View File

@ -1,11 +1,15 @@
*~ *~
*.egg-info *.egg-info
*.egg *.egg
*.eggs
*.pyc
.coverage .coverage
.coverage.* .coverage.*
coverage coverage
.tox .tox
settings.py settings.py
*.pyc
.idea/* .idea/*
.cache/* .cache/*
.python-version
docs/_build/
docs/_api/

View File

@ -1,96 +0,0 @@
# Sanic
[![Join the chat at https://gitter.im/sanic-python/Lobby](https://badges.gitter.im/sanic-python/Lobby.svg)](https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Build Status](https://travis-ci.org/channelcat/sanic.svg?branch=master)](https://travis-ci.org/channelcat/sanic)
[![PyPI](https://img.shields.io/pypi/v/sanic.svg)](https://pypi.python.org/pypi/sanic/)
[![PyPI](https://img.shields.io/pypi/pyversions/sanic.svg)](https://pypi.python.org/pypi/sanic/)
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by this article: https://magic.io/blog/uvloop-blazing-fast-python-networking/.
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
## Benchmarks
All tests were run on an AWS medium instance running ubuntu, using 1 process. Each script delivered a small JSON response and was tested with wrk using 100 connections. Pypy was tested for Falcon and Flask but did not speed up requests.
| Server | Implementation | Requests/sec | Avg Latency |
| ------- | ------------------- | ------------:| -----------:|
| Sanic | Python 3.5 + uvloop | 33,342 | 2.96ms |
| Wheezy | gunicorn + meinheld | 20,244 | 4.94ms |
| Falcon | gunicorn + meinheld | 18,972 | 5.27ms |
| Bottle | gunicorn + meinheld | 13,596 | 7.36ms |
| Flask | gunicorn + meinheld | 4,988 | 20.08ms |
| Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms |
| Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms |
| Tornado | Python 3.5 | 2,138 | 46.66ms |
## Hello World
```python
from sanic import Sanic
from sanic.response import json
app = Sanic()
@app.route("/")
async def test(request):
return json({"hello": "world"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
```
## Installation
* `python -m pip install sanic`
## Documentation
* [Getting started](docs/getting_started.md)
* [Request Data](docs/request_data.md)
* [Routing](docs/routing.md)
* [Middleware](docs/middleware.md)
* [Exceptions](docs/exceptions.md)
* [Blueprints](docs/blueprints.md)
* [Class Based Views](docs/class_based_views.md)
* [Cookies](docs/cookies.md)
* [Static Files](docs/static_files.md)
* [Testing](docs/testing.md)
* [Deploying](docs/deploying.md)
* [Contributing](docs/contributing.md)
* [License](LICENSE)
## TODO:
* Streamed file processing
* File output
* Examples of integrations with 3rd-party modules
* RESTful router
## Limitations:
* No wheels for uvloop and httptools on Windows :(
## Final Thoughts:
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
▌ ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
▀▀█████████▀
▄▄██▀██████▀█
▄██▀ ▀▀▀ █
▄█ ▐▌
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
▌ ▐ ▀▀▄▄▄▀
▀▀▄▄▀

127
README.rst Normal file
View File

@ -0,0 +1,127 @@
Sanic
=================================
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |PyPI| |PyPI version|
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article <https://magic.io/blog/uvloop-blazing-fast-python-networking/>`_.
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
Benchmarks
----------
All tests were run on an AWS medium instance running ubuntu, using 1
process. Each script delivered a small JSON response and was tested with
wrk using 100 connections. Pypy was tested for Falcon and Flask but did
not speed up requests.
+-----------+-----------------------+----------------+---------------+
| Server | Implementation | Requests/sec | Avg Latency |
+===========+=======================+================+===============+
| Sanic | Python 3.5 + uvloop | 33,342 | 2.96ms |
+-----------+-----------------------+----------------+---------------+
| Wheezy | gunicorn + meinheld | 20,244 | 4.94ms |
+-----------+-----------------------+----------------+---------------+
| Falcon | gunicorn + meinheld | 18,972 | 5.27ms |
+-----------+-----------------------+----------------+---------------+
| Bottle | gunicorn + meinheld | 13,596 | 7.36ms |
+-----------+-----------------------+----------------+---------------+
| Flask | gunicorn + meinheld | 4,988 | 20.08ms |
+-----------+-----------------------+----------------+---------------+
| Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms |
+-----------+-----------------------+----------------+---------------+
| Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms |
+-----------+-----------------------+----------------+---------------+
| Tornado | Python 3.5 | 2,138 | 46.66ms |
+-----------+-----------------------+----------------+---------------+
Hello World Example
-------------------
.. code:: python
from sanic import Sanic
from sanic.response import json
app = Sanic()
@app.route("/")
async def test(request):
return json({"hello": "world"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
SSL Example
-----------
Optionally pass in an SSLContext:
.. code:: python
import ssl
certificate = "/path/to/certificate"
keyfile = "/path/to/keyfile"
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(certificate, keyfile=keyfile)
app.run(host="0.0.0.0", port=8443, ssl=context)
Installation
------------
- ``python -m pip install sanic``
Documentation
-------------
Documentation can be found in the ``docs`` directory.
.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |Build Status| image:: https://travis-ci.org/channelcat/sanic.svg?branch=master
:target: https://travis-ci.org/channelcat/sanic
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
:target: https://pypi.python.org/pypi/sanic/
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
:target: https://pypi.python.org/pypi/sanic/
TODO
----
* Streamed file processing
* File output
* Examples of integrations with 3rd-party modules
* RESTful router
Limitations
-----------
* No wheels for uvloop and httptools on Windows :(
Final Thoughts
--------------
::
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
▌ ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
▀▀█████████▀
▄▄██▀██████▀█
▄██▀ ▀▀▀ █
▄█ ▐▌
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
▌ ▐ ▀▀▄▄▄▀
▀▀▄▄▀

View File

@ -1,25 +1,20 @@
# Blueprints # Blueprints
Blueprints are objects that can be used for sub-routing within an application. Blueprints are objects that can be used for sub-routing within an application.
Instead of adding routes to the application object, blueprints define similar Instead of adding routes to the application instance, blueprints define similar
methods for adding routes, which are then registered with the application in a methods for adding routes, which are then registered with the application in a
flexible and pluggable manner. flexible and pluggable manner.
## Why? Blueprints are especially useful for larger applications, where your
application logic can be broken down into several groups or areas of
Blueprints are especially useful for larger applications, where your application responsibility.
logic can be broken down into several groups or areas of responsibility.
It is also useful for API versioning, where one blueprint may point at
`/v1/<routes>`, and another pointing at `/v2/<routes>`.
## My First Blueprint ## My First Blueprint
The following shows a very simple blueprint that registers a handler-function at The following shows a very simple blueprint that registers a handler-function at
the root `/` of your application. the root `/` of your application.
Suppose you save this file as `my_blueprint.py`, this can be imported in your Suppose you save this file as `my_blueprint.py`, which can be imported into your
main application later. main application later.
```python ```python
@ -34,7 +29,8 @@ async def bp_root(request):
``` ```
## Registering Blueprints ## Registering blueprints
Blueprints must be registered with the application. Blueprints must be registered with the application.
```python ```python
@ -48,14 +44,19 @@ app.run(host='0.0.0.0', port=8000, debug=True)
``` ```
This will add the blueprint to the application and register any routes defined This will add the blueprint to the application and register any routes defined
by that blueprint. by that blueprint. In this example, the registered routes in the `app.router`
In this example, the registered routes in the `app.router` will look like: will look like:
```python ```python
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=None, pattern=re.compile('^/$'), parameters=[])] [Route(handler=<function bp_root at 0x7f908382f9d8>, methods=None, pattern=re.compile('^/$'), parameters=[])]
``` ```
## Middleware ## Using blueprints
Blueprints have much the same functionality as an application instance.
### Middleware
Using blueprints allows you to also register middleware globally. Using blueprints allows you to also register middleware globally.
```python ```python
@ -72,30 +73,36 @@ async def halt_response(request, response):
return text('I halted the response') return text('I halted the response')
``` ```
## Exceptions ### Exceptions
Exceptions can also be applied exclusively to blueprints globally.
Exceptions can be applied exclusively to blueprints globally.
```python ```python
@bp.exception(NotFound) @bp.exception(NotFound)
def ignore_404s(request, exception): def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url)) return text("Yep, I totally found the page: {}".format(request.url))
```
## Static files ### Static files
Static files can also be served globally, under the blueprint prefix.
Static files can be served globally, under the blueprint prefix.
```python ```python
bp.static('/folder/to/serve', '/web/path') bp.static('/folder/to/serve', '/web/path')
``` ```
## Start and Stop ## Start and stop
Blueprints and run functions during the start and stop process of the server.
If running in multiprocessor mode (more than 1 worker), these are triggered after the workers fork Blueprints can run functions during the start and stop process of the server.
If running in multiprocessor mode (more than 1 worker), these are triggered
after the workers fork.
Available events are: Available events are:
* before_server_start - Executed before the server begins to accept connections - `before_server_start`: Executed before the server begins to accept connections
* after_server_start - Executed after the server begins to accept connections - `after_server_start`: Executed after the server begins to accept connections
* before_server_stop - Executed before the server stops accepting connections - `before_server_stop`: Executed before the server stops accepting connections
* after_server_stop - Executed after the server is stopped and all requests are complete - `after_server_stop`: Executed after the server is stopped and all requests are complete
```python ```python
bp = Blueprint('my_blueprint') bp = Blueprint('my_blueprint')
@ -109,3 +116,49 @@ async def setup_connection():
async def close_connection(): async def close_connection():
await database.close() await database.close()
``` ```
## Use-case: API versioning
Blueprints can be very useful for API versioning, where one blueprint may point
at `/v1/<routes>`, and another pointing at `/v2/<routes>`.
When a blueprint is initialised, it can take an optional `url_prefix` argument,
which will be prepended to all routes defined on the blueprint. This feature
can be used to implement our API versioning scheme.
```python
# blueprints.py
from sanic.response import text
from sanic import Blueprint
blueprint_v1 = Blueprint('v1')
blueprint_v2 = Blueprint('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')
```
When we register our blueprints on the app, the routes `/v1` and `/v2` will now
point to the individual blueprints, which allows the creation of *sub-sites*
for each API version.
```python
# main.py
from sanic import Sanic
from blueprints import blueprint_v1, blueprint_v2
app = Sanic(__name__)
app.blueprint(blueprint_v1)
app.blueprint(blueprint_v2)
app.run(host='0.0.0.0', port=8000, debug=True)
```
**Previous:** [Exceptions](exceptions.md)
**Next:** [Class-based views](class_based_views.md)

View File

@ -1,8 +1,25 @@
# Class based views # Class-Based Views
Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone tries to use a method that has not been implemented, there will be 405 response. Class-based views are simply classes which implement response behaviour to
requests. They provide a way to compartmentalise handling of different HTTP
request types at the same endpoint. Rather than defining and decorating three
different handler functions, one for each of an endpoint's supported request
type, the endpoint can be assigned a class-based view.
## Defining views
A class-based view should subclass `HTTPMethodView`. You can then implement
class methods for every HTTP request type you want to support. If a request is
received that has no defined method, a `405: Method not allowed` response will
be generated.
To register a class-based view on an endpoint, the `app.add_route` method is
used. The first argument should be the defined class with the method `as_view`
invoked, and the second should be the URL endpoint.
The available methods are `get`, `post`, `put`, `patch`, and `delete`. A class
using all these methods would look like the following.
## Examples
```python ```python
from sanic import Sanic from sanic import Sanic
from sanic.views import HTTPMethodView from sanic.views import HTTPMethodView
@ -10,7 +27,6 @@ from sanic.response import text
app = Sanic('some_name') app = Sanic('some_name')
class SimpleView(HTTPMethodView): class SimpleView(HTTPMethodView):
def get(self, request): def get(self, request):
@ -28,11 +44,14 @@ class SimpleView(HTTPMethodView):
def delete(self, request): def delete(self, request):
return text('I am delete method') return text('I am delete method')
app.add_route(SimpleView(), '/') app.add_route(SimpleView.as_view(), '/')
``` ```
If you need any url params just mention them in method definition: ## URL parameters
If you need any URL parameters, as discussed in the routing guide, include them
in the method definition.
```python ```python
class NameView(HTTPMethodView): class NameView(HTTPMethodView):
@ -40,6 +59,24 @@ class NameView(HTTPMethodView):
def get(self, request, name): def get(self, request, name):
return text('Hello {}'.format(name)) return text('Hello {}'.format(name))
app.add_route(NameView(), '/<name>') app.add_route(NameView.as_view(), '/<name>')
```
## Decorators
If you want to add any decorators to the class, you can set the `decorators`
class variable. These will be applied to the class when `as_view` is called.
``` ```
class ViewWithDecorator(HTTPMethodView):
decorators = [some_decorator_here]
def get(self, request, name):
return text('Hello I have a decorator')
app.add_route(ViewWithDecorator.as_view(), '/url')
```
**Previous:** [Blueprints](blueprints.md)
**Next:** [Cookies](cookies.md)

155
docs/conf.py Normal file
View File

@ -0,0 +1,155 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Sanic documentation build configuration file, created by
# sphinx-quickstart on Sun Dec 25 18:07:21 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
import os
import sys
# Add support for Markdown documentation using Recommonmark
from recommonmark.parser import CommonMarkParser
# Ensure that sanic is present in the path, to allow sphinx-apidoc to
# autogenerate documentation from docstrings
root_directory = os.path.dirname(os.getcwd())
sys.path.insert(0, root_directory)
import sanic
# -- General configuration ------------------------------------------------
extensions = ['sphinx.ext.autodoc',
'sphinx.ext.viewcode',
'sphinx.ext.githubpages']
templates_path = ['_templates']
# Enable support for both Restructured Text and Markdown
source_parsers = {'.md': CommonMarkParser}
source_suffix = ['.rst', '.md']
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'Sanic'
copyright = '2016, Sanic contributors'
author = 'Sanic contributors'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = sanic.__version__
# The full version, including alpha/beta/rc tags.
release = sanic.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'en'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
#
# modules.rst is generated by sphinx-apidoc but is unused. This suppresses
# a warning about it.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'modules.rst']
# The name of the Pygments (syntax highlighting) style to use.
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
# a list of builtin themes.
html_theme = 'alabaster'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# 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 = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# 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'),
]
# -- 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)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (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'),
]
# -- Options for Epub output ----------------------------------------------
# Bibliographic Dublin Core info.
epub_title = project
epub_author = author
epub_publisher = author
epub_copyright = copyright
# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']

View File

@ -1,10 +1,35 @@
# How to contribute to Sanic # Contributing
Thank you for your interest! Thank you for your interest! Sanic is always looking for contributors. If you
don't feel comfortable contributing code, adding docstrings to the source files
is very appreciated.
## Running tests ## Running tests
* `python -m pip install pytest` * `python -m pip install pytest`
* `python -m pytest tests` * `python -m pytest tests`
## Documentation
Sanic's documentation is built
using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in
Markdown and can be found in the `docs` folder, while the module reference is
automatically generated using `sphinx-apidoc`.
To generate the documentation from scratch:
```bash
sphinx-apidoc -fo docs/_api/ sanic
sphinx-build -b html docs docs/_build
```
The HTML documentation will be created in the `docs/_build` folder.
## Warning ## Warning
One of the main goals of Sanic is speed. Code that lowers the performance of Sanic without significant gains in usability, security, or features may not be merged.
One of the main goals of Sanic is speed. Code that lowers the performance of
Sanic without significant gains in usability, security, or features may not be
merged. Please don't let this intimidate you! If you have any concerns about an
idea, open an issue for discussion and help.
**Previous:** [Sanic extensions](extensions.md)

View File

@ -1,13 +1,13 @@
# Cookies # Cookies
## Request Cookies are pieces of data which persist inside a user's browser. Sanic can
both read and write cookies, which are stored as key-value pairs.
Request cookies can be accessed via the request.cookie dictionary ## Reading cookies
### Example A user's cookies can be accessed `Request` object's `cookie` dictionary.
```python ```python
from sanic import Sanic
from sanic.response import text from sanic.response import text
@app.route("/cookie") @app.route("/cookie")
@ -16,28 +16,11 @@ async def test(request):
return text("Test cookie set to: {}".format(test_cookie)) return text("Test cookie set to: {}".format(test_cookie))
``` ```
## Response ## Writing cookies
Response cookies can be set like dictionary values and When returning a response, cookies can be set on the `Response` object.
have the following parameters available:
* expires - datetime - Time for cookie to expire on the client's browser
* path - string - The Path attribute specifies the subset of URLs to
which this cookie applies
* comment - string - Cookie comment (metadata)
* domain - string - Specifies the domain for which the
cookie is valid. An explicitly specified domain must always
start with a dot.
* max-age - number - Number of seconds the cookie should live for
* secure - boolean - Specifies whether the cookie will only be sent via
HTTPS
* httponly - boolean - Specifies whether the cookie cannot be read
by javascript
### Example
```python ```python
from sanic import Sanic
from sanic.response import text from sanic.response import text
@app.route("/cookie") @app.route("/cookie")
@ -48,3 +31,22 @@ async def test(request):
response.cookies['test']['httponly'] = True response.cookies['test']['httponly'] = True
return response return response
``` ```
Response cookies can be set like dictionary values and have the following
parameters available:
- `expires` (datetime): The time for the cookie to expire on the
client's browser.
- `path` (string): The subset of URLs to which this cookie applies.
- `comment` (string): A comment (metadata).
- `domain` (string): Specifies the domain for which the cookie is valid. An
explicitly specified domain must always start with a dot.
- `max-age` (number): Number of seconds the cookie should live for.
- `secure` (boolean): Specifies whether the cookie will only be sent via
HTTPS.
- `httponly` (boolean): Specifies whether the cookie cannot be read by
Javascript.
**Previous:** [Class-based views](class_based_views.md)
**Next:** [Custom protocols](custom_protocol.md)

76
docs/custom_protocol.md Normal file
View File

@ -0,0 +1,76 @@
# Custom Protocols
*Note: this is advanced usage, and most readers will not need such functionality.*
You can change the behavior of Sanic's protocol by specifying a custom
protocol, which should be a subclass
of
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
This protocol can then be passed as the keyword argument `protocol` to the `sanic.run` method.
The constructor of the custom protocol class receives the following keyword
arguments from Sanic.
- `loop`: an `asyncio`-compatible event loop.
- `connections`: a `set` to store protocol objects. When Sanic receives
`SIGINT` or `SIGTERM`, it executes `protocol.close_if_idle` for all protocol
objects stored in this set.
- `signal`: a `sanic.server.Signal` object with the `stopped` attribute. When
Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` is assigned `True`.
- `request_handler`: a coroutine that takes a `sanic.request.Request` object
and a `response` callback as arguments.
- `error_handler`: a `sanic.exceptions.Handler` which is called when exceptions
are raised.
- `request_timeout`: the number of seconds before a request times out.
- `request_max_size`: an integer specifying the maximum size of a request, in bytes.
## Example
An error occurs in the default protocol if a handler function does not return
an `HTTPResponse` object.
By overriding the `write_response` protocol method, if a handler returns a
string it will be converted to an `HTTPResponse object`.
```python
from sanic import Sanic
from sanic.server import HttpProtocol
from sanic.response import text
app = Sanic(__name__)
class CustomHttpProtocol(HttpProtocol):
def __init__(self, *, loop, request_handler, error_handler,
signal, connections, request_timeout, request_max_size):
super().__init__(
loop=loop, request_handler=request_handler,
error_handler=error_handler, signal=signal,
connections=connections, request_timeout=request_timeout,
request_max_size=request_max_size)
def write_response(self, response):
if isinstance(response, str):
response = text(response)
self.transport.write(
response.output(self.request.version)
)
self.transport.close()
@app.route('/')
async def string(request):
return 'string'
@app.route('/1')
async def response(request):
return text('response')
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)
```
**Previous:** [Cookies](cookies.md)
**Next:** [Testing](testing.md)

View File

@ -1,35 +1,59 @@
# Deploying # Deploying
When it comes to deploying Sanic, there's not much to it, but there are Deploying Sanic is made simple by the inbuilt webserver. After defining an
a few things to take note of. instance of `sanic.Sanic`, we can call the `run` method with the following
keyword arguments:
- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
- `port` *(default `8000`)*: Port to host the server on.
- `debug` *(default `False`)*: Enables debug output (slows server).
- `before_start` *(default `None`)*: Function or list of functions to be executed
before the server starts accepting connections.
- `after_start` *(default `None`)*: Function or list of functions to be executed
after the server starts accepting connections.
- `before_stop` *(default `None`)*: Function or list of functions to be
executed when a stop signal is received before it is
respected.
- `after_stop` *(default `None`)*: Function or list of functions to be executed
when all requests are complete.
- `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s).
- `sock` *(default `None`)*: Socket for the server to accept connections from.
- `workers` *(default `1`)*: Number of worker processes to spawn.
- `loop` *(default `None`)*: An `asyncio`-compatible event loop. If none is
specified, Sanic creates its own event loop.
- `protocol` *(default `HttpProtocol`)*: Subclass
of
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
## Workers ## Workers
By default, Sanic listens in the main process using only 1 CPU core. By default, Sanic listens in the main process using only one CPU core. To crank
To crank up the juice, just specify the number of workers in the run up the juice, just specify the number of workers in the `run` arguments.
arguments like so:
```python ```python
app.run(host='0.0.0.0', port=1337, workers=4) app.run(host='0.0.0.0', port=1337, workers=4)
``` ```
Sanic will automatically spin up multiple processes and route Sanic will automatically spin up multiple processes and route traffic between
traffic between them. We recommend as many workers as you have them. We recommend as many workers as you have available cores.
available cores.
## Running via Command ## Running via command
If you like using command line arguments, you can launch a sanic server If you like using command line arguments, you can launch a Sanic server by
by executing the module. For example, if you initialized sanic as executing the module. For example, if you initialized Sanic as `app` in a file
app in a file named server.py, you could run the server like so: named `server.py`, you could run the server like so:
`python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4` `python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4`
With this way of running sanic, it is not necessary to run app.run in With this way of running sanic, it is not necessary to invoke `app.run` in your
your python file. If you do, just make sure you wrap it in name == main Python file. If you do, make sure you wrap it so that it only executes when
like so: directly run by the interpreter.
```python ```python
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337, workers=4) app.run(host='0.0.0.0', port=1337, workers=4)
``` ```
**Previous:** [Request Data](request_data.md)
**Next:** [Static Files](static_files.md)

View File

@ -1,24 +1,31 @@
# Exceptions # Exceptions
Exceptions can be thrown from within request handlers and will automatically be handled by Sanic. Exceptions take a message as their first argument, and can also take a status_code to be passed back in the HTTP response. Check sanic.exceptions for the full list of exceptions to throw. Exceptions can be thrown from within request handlers and will automatically be
handled by Sanic. Exceptions take a message as their first argument, and can
also take a status code to be passed back in the HTTP response.
## Throwing an exception ## Throwing an exception
To throw an exception, simply `raise` the relevant exception from the
`sanic.exceptions` module.
```python ```python
from sanic import Sanic
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
@app.route('/killme') @app.route('/killme')
def i_am_ready_to_die(request): def i_am_ready_to_die(request):
raise ServerError("Something bad happened") raise ServerError("Something bad happened", status_code=500)
``` ```
## Handling Exceptions ## Handling exceptions
Just use the @exception decorator. The decorator expects a list of exceptions to handle as arguments. You can pass SanicException to catch them all! The exception handler must expect a request and exception object as arguments. To override Sanic's default handling of an exception, the `@app.exception`
decorator is used. The decorator expects a list of exceptions to handle as
arguments. You can pass `SanicException` to catch them all! The decorated
exception handler function must take a `Request` and `Exception` object as
arguments.
```python ```python
from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.exceptions import NotFound from sanic.exceptions import NotFound
@ -26,3 +33,17 @@ from sanic.exceptions import NotFound
def ignore_404s(request, exception): def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url)) return text("Yep, I totally found the page: {}".format(request.url))
``` ```
## Useful exceptions
Some of the most useful exceptions are presented below:
- `NotFound`: called when a suitable route for the request isn't found.
- `ServerError`: called when something goes wrong inside the server. This
usually occurs if there is an exception raised in user code.
See the `sanic.exceptions` module for the full list of exceptions to throw.
**Previous:** [Middleware](middleware.md)
**Next:** [Blueprints](blueprints.md)

11
docs/extensions.md Normal file
View File

@ -0,0 +1,11 @@
# Sanic Extensions
A list of Sanic extensions created by the community.
- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions.
Allows using redis, memcache or an in memory store.
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
**Previous:** [Testing](testing.md)
**Next:** [Contributing](contributing.md)

View File

@ -1,25 +1,29 @@
# Getting Started # Getting Started
Make sure you have pip and python 3.5 before starting Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at
least version 3.5 of Python before starting. Sanic uses the new `async`/`await`
syntax, so earlier versions of python won't work.
## Benchmarks 1. Install Sanic: `python3 -m pip install sanic`
* Install Sanic 2. Create a file called `main.py` with the following code:
* `python3 -m pip install sanic`
* Edit main.py to include:
```python
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__) ```python
from sanic import Sanic
from sanic.response import text
@app.route("/") app = Sanic(__name__)
async def test(request):
return json({ "hello": "world" })
app.run(host="0.0.0.0", port=8000, debug=True) @app.route("/")
``` async def test(request):
* Run `python3 main.py` return text('Hello world!')
You now have a working Sanic server! To continue on, check out: app.run(host="0.0.0.0", port=8000, debug=True)
* [Request Data](request_data.md) ```
* [Routing](routing.md)
3. Run the server: `python3 main.py`
4. Open the address `http://0.0.0.0:8000` in your web browser. You should see
the message *Hello world!*.
You now have a working Sanic server!
**Next:** [Routing](routing.md)

33
docs/index.rst Normal file
View File

@ -0,0 +1,33 @@
.. include:: ../README.rst
Guides
======
.. toctree::
:maxdepth: 2
getting_started
routing
request_data
deploying
static_files
middleware
exceptions
blueprints
class_based_views
cookies
custom_protocol
testing
extensions
contributing
Module Documentation
====================
.. toctree::
Module Reference <_api/sanic>
* :ref:`genindex`
* :ref:`search`

View File

@ -1,36 +1,32 @@
# Middleware # Middleware
Middleware can be executed before or after requests. It is executed in the order it was registered. If middleware returns a response object, the request will stop processing and a response will be returned. 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.
Middleware is registered via the middleware decorator, and can either be added as 'request' or 'response' middleware, based on the argument provided in the decorator. Response middleware receives both the request and the response as arguments. 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
receives both the request and the response as arguments.
## Examples
The simplest middleware doesn't modify the request or response at all:
```python ```python
app = Sanic(__name__)
@app.middleware
async def halt_request(request):
print("I am a spy")
@app.middleware('request') @app.middleware('request')
async def halt_request(request): async def print_on_request(request):
return text('I halted the request') print("I print when a request is received by the server")
@app.middleware('response') @app.middleware('response')
async def halt_response(request, response): async def print_on_response(request, response):
return text('I halted the response') print("I print when a response is returned by the server")
@app.route('/')
async def handler(request):
return text('I would like to speak now please')
app.run(host="0.0.0.0", port=8000)
``` ```
## Middleware chain ## Modifying the request or response
If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that you do **not return** any response in your middleware: Middleware can modify the request or response parameter it is given, *as long
as it does not return it*. The following example shows a practical use-case for
this.
```python ```python
app = Sanic(__name__) app = Sanic(__name__)
@ -46,4 +42,29 @@ async def prevent_xss(request, response):
app.run(host="0.0.0.0", port=8000) app.run(host="0.0.0.0", port=8000)
``` ```
The above code will apply the two middlewares in order. First the middleware **custom_banner** will change the HTTP Response headers *Server* by *Fake-Server*, and the second middleware **prevent_xss** will add the HTTP Headers for prevent Cross-Site-Scripting (XSS) attacks. The above code will apply the two middleware in order. First, the middleware
**custom_banner** will change the HTTP response header *Server* to
*Fake-Server*, and the second middleware **prevent_xss** will add the HTTP
header for preventing Cross-Site-Scripting (XSS) attacks. These two functions
are invoked *after* a user function returns a response.
## Responding early
If middleware returns a `HTTPResponse` object, the request will stop processing
and the response will be returned. If this occurs to a request before the
relevant user route handler is reached, the handler will never be called.
Returning a response will also prevent any further middleware from running.
```python
@app.middleware('request')
async def halt_request(request):
return text('I halted the request')
@app.middleware('response')
async def halt_response(request, response):
return text('I halted the response')
```
**Previous:** [Static Files](static_files.md)
**Next:** [Exceptions](exceptions.md)

View File

@ -1,49 +1,95 @@
# Request Data # Request Data
## Properties When an endpoint receives a HTTP request, the route function is passed a
`Request` object.
The following request variables are accessible as properties: The following variables are accessible as properties on `Request` objects:
`request.files` (dictionary of File objects) - List of files that have a name, body, and type - `json` (any) - JSON body
`request.json` (any) - JSON body
`request.args` (dict) - Query String variables. Use getlist to get multiple of the same name
`request.form` (dict) - Posted form variables. Use getlist to get multiple of the same name
`request.body` (bytes) - Posted raw body. To get the raw data, regardless of content type
See request.py for more information ```python
from sanic.response import json
## Examples @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 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 })
```
- `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')
file_parameters = {
'body': test_file.body,
'name': test_file.name,
'type': test_file.type,
}
return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters })
```
- `form` (dict) - Posted form variables.
```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') })
```
- `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.
## Accessing values using `get` and `getlist`
The request properties which return a dictionary actually return a subclass of
`dict` called `RequestParameters`. The key difference when using this object is
the distinction between the `get` and `getlist` methods.
- `get(key, default=None)` operates as normal, except that when the value of
the given key is a list, *only the first item is returned*.
- `getlist(key, default=None)` operates as normal, *returning the entire list*.
```python ```python
from sanic import Sanic from sanic.request import RequestParameters
from sanic.response import json, text
@app.route("/json") args = RequestParameters()
def post_json(request): args['titles'] = ['Post 1', 'Post 2']
return json({ "received": True, "message": request.json })
@app.route("/form") args.get('titles') # => 'Post 1'
def post_json(request):
return json({ "received": True, "form_data": request.form, "test": request.form.get('test') })
@app.route("/files") args.getlist('titles') # => ['Post 1', 'Post 2']
def post_json(request):
test_file = request.files.get('test')
file_parameters = {
'body': test_file.body,
'name': test_file.name,
'type': test_file.type,
}
return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters })
@app.route("/query_string")
def query_string(request):
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
@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)
``` ```
**Previous:** [Routing](routing.md)
**Next:** [Deploying](deploying.md)

View File

@ -1,17 +1,48 @@
# Routing # Routing
Sanic comes with a basic router that supports request parameters. To specify a parameter, surround it with carrots like so: `<PARAM>`. Request parameters will be passed to the request handler functions as keyword arguments. To specify a type, add a :type after the parameter name, in the carrots. If the parameter does not match the type supplied, Sanic will throw a NotFound exception, resulting in a 404 page not found error. Routing allows the user to specify handler functions for different URL endpoints.
A basic route looks like the following, where `app` is an instance of the
## Examples `Sanic` class:
```python
from sanic.response import json
@app.route("/")
async def test(request):
return json({ "hello": "world" })
```
When the url `http://server.url/` is accessed (the base url of the server), the
final `/` is matched by the router to the handler function, `test`, which then
returns a JSON object.
Sanic handler functions must be defined using the `async def` syntax, as they
are asynchronous functions.
## Request parameters
Sanic comes with a basic router that supports request parameters.
To specify a parameter, surround it with angle quotes like so: `<PARAM>`.
Request parameters will be passed to the route handler functions as keyword
arguments.
```python ```python
from sanic import Sanic
from sanic.response import text from sanic.response import text
@app.route('/tag/<tag>') @app.route('/tag/<tag>')
async def tag_handler(request, tag): async def tag_handler(request, tag):
return text('Tag - {}'.format(tag)) return text('Tag - {}'.format(tag))
```
To specify a type for the parameter, add a `:type` after the parameter name,
inside the quotes. If the parameter does not match the specified type, Sanic
will throw a `NotFound` exception, resulting in a `404: Page not found` error
on the URL.
```python
from sanic.response import text
@app.route('/number/<integer_arg:int>') @app.route('/number/<integer_arg:int>')
async def integer_handler(request, integer_arg): async def integer_handler(request, integer_arg):
@ -29,16 +60,52 @@ async def person_handler(request, name):
async def folder_handler(request, folder_id): async def folder_handler(request, folder_id):
return text('Folder - {}'.format(folder_id)) return text('Folder - {}'.format(folder_id))
async def handler1(request): ```
return text('OK')
app.add_route(handler1, '/test')
async def handler(request, name): ## HTTP request types
return text('Folder - {}'.format(name))
app.add_route(handler, '/folder/<name>')
async def person_handler(request, name): By default, a route defined on a URL will be used for all requests to that URL.
return text('Person - {}'.format(name)) However, the `@app.route` decorator accepts an optional parameter, `methods`,
app.add_route(handler, '/person/<name:[A-z]>') which restricts the handler function to the HTTP methods in the given list.
```python
from sanic.response import text
@app.route('/post')
async def post_handler(request, methods=['POST']):
return text('POST request - {}'.format(request.json))
@app.route('/get')
async def GET_handler(request, methods=['GET']):
return text('GET request - {}'.format(request.args))
``` ```
## The `add_route` method
As we have seen, routes are often specified using the `@app.route` decorator.
However, this decorator is really just a wrapper for the `app.add_route`
method, which is used as follows:
```python
from sanic.response import text
# Define the handler functions
async def handler1(request):
return text('OK')
async def handler2(request, name):
return text('Folder - {}'.format(name))
async def person_handler2(request, name):
return text('Person - {}'.format(name))
# Add each handler function as a route
app.add_route(handler1, '/test')
app.add_route(handler2, '/folder/<name>')
app.add_route(person_handler2, '/person/<name:[A-z]>', methods=['GET'])
```
**Previous:** [Getting Started](getting_started.md)
**Next:** [Request Data](request_data.md)

View File

@ -1,10 +1,11 @@
# Static Files # Static Files
Both directories and files can be served by registering with static Static files and directories, such as an image file, are served by Sanic when
registered with the `app.static` method. The method takes an endpoint URL and a
## Example filename. The file specified will then be accessible via the given endpoint.
```python ```python
from sanic import Sanic
app = Sanic(__name__) app = Sanic(__name__)
# Serves files from the static folder to the URL /static # Serves files from the static folder to the URL /static
@ -16,3 +17,7 @@ app.static('/the_best.png', '/home/ubuntu/test.png')
app.run(host="0.0.0.0", port=8000) app.run(host="0.0.0.0", port=8000)
``` ```
**Previous:** [Deploying](deploying.md)
**Next:** [Middleware](middleware.md)

View File

@ -49,3 +49,7 @@ def test_endpoint_challenge():
# Assert that the server responds with the challenge string # Assert that the server responds with the challenge string
assert response.text == request_data['challenge'] assert response.text == request_data['challenge']
``` ```
**Previous:** [Custom protocols](custom_protocol.md)
**Next:** [Sanic extensions](extensions.md)

View File

@ -9,17 +9,15 @@ and pass in an instance of it when we create our Sanic instance. Inside this
class' default handler, we can do anything including sending exceptions to class' default handler, we can do anything including sending exceptions to
an external service. an external service.
""" """
from sanic.exceptions import Handler, SanicException
""" """
Imports and code relevant for our CustomHandler class Imports and code relevant for our CustomHandler class
(Ordinarily this would be in a separate file) (Ordinarily this would be in a separate file)
""" """
from sanic.response import text
from sanic.exceptions import Handler, SanicException
class CustomHandler(Handler): class CustomHandler(Handler):
def default(self, request, exception): def default(self, request, exception):
# Here, we have access to the exception object # Here, we have access to the exception object
# and can do anything with it (log, send to external service, etc) # and can do anything with it (log, send to external service, etc)
@ -31,9 +29,7 @@ class CustomHandler(Handler):
# Then, we must finish handling the exception by returning # Then, we must finish handling the exception by returning
# our response to the client # our response to the client
# For this we can just call the super class' default handler # For this we can just call the super class' default handler
return super.default(self, request, exception) return super().default(request, exception)
""" """
@ -49,11 +45,12 @@ app = Sanic(__name__)
handler = CustomHandler(sanic=app) handler = CustomHandler(sanic=app)
app.error_handler = handler app.error_handler = handler
@app.route("/") @app.route("/")
async def test(request): async def test(request):
# Here, something occurs which causes an unexpected exception # Here, something occurs which causes an unexpected exception
# This exception will flow to our custom handler. # This exception will flow to our custom handler.
x = 1 / 0 1 / 0
return json({"test": True}) return json({"test": True})

View File

@ -14,7 +14,7 @@ logging.basicConfig(
log = logging.getLogger() log = logging.getLogger()
# Set logger to override default basicConfig # Set logger to override default basicConfig
sanic = Sanic(logger=True) sanic = Sanic()
@sanic.route("/") @sanic.route("/")
def test(request): def test(request):
log.info("received request; responding with 'hey'") log.info("received request; responding with 'hey'")

View File

@ -0,0 +1,65 @@
""" To run this example you need additional asyncpg package
"""
import os
import asyncio
import uvloop
from asyncpg import create_pool
from sanic import Sanic
from sanic.response import json
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
DB_CONFIG = {
'host': '<host>',
'user': '<username>',
'password': '<password>',
'port': '<port>',
'database': '<database>'
}
def jsonify(records):
""" Parse asyncpg record response into JSON format
"""
return [{key: value for key, value in
zip(r.keys(), r.values())} for r in records]
loop = asyncio.get_event_loop()
async def make_pool():
return await create_pool(**DB_CONFIG)
app = Sanic(__name__)
pool = loop.run_until_complete(make_pool())
async def create_db():
""" Create some table and add some data
"""
async with pool.acquire() as connection:
async with connection.transaction():
await connection.execute('DROP TABLE IF EXISTS sanic_post')
await connection.execute("""CREATE TABLE sanic_post (
id serial primary key,
content varchar(50),
post_date timestamp
);""")
for i in range(0, 100):
await connection.execute(f"""INSERT INTO sanic_post
(id, content, post_date) VALUES ({i}, {i}, now())""")
@app.route("/")
async def handler(request):
async with pool.acquire() as connection:
async with connection.transaction():
results = await connection.fetch('SELECT * FROM sanic_post')
return json({'posts': jsonify(results)})
if __name__ == '__main__':
loop.run_until_complete(create_db())
app.run(host='0.0.0.0', port=8000, loop=loop)

View File

@ -64,11 +64,11 @@ def query_string(request):
# Run Server # Run Server
# ----------------------------------------------- # # ----------------------------------------------- #
def after_start(loop): def after_start(app, loop):
log.info("OH OH OH OH OHHHHHHHH") log.info("OH OH OH OH OHHHHHHHH")
def before_stop(loop): def before_stop(app, loop):
log.info("TRIED EVERYTHING") log.info("TRIED EVERYTHING")

39
examples/vhosts.py Normal file
View File

@ -0,0 +1,39 @@
from sanic.response import text
from sanic import Sanic
from sanic.blueprints import Blueprint
# Usage
# curl -H "Host: example.com" localhost:8000
# curl -H "Host: sub.example.com" localhost:8000
# curl -H "Host: bp.example.com" localhost:8000/question
# curl -H "Host: bp.example.com" localhost:8000/answer
app = Sanic()
bp = Blueprint("bp", host="bp.example.com")
@app.route('/', host=["example.com",
"somethingelse.com",
"therestofyourdomains.com"])
async def hello(request):
return text("Some defaults")
@app.route('/', host="example.com")
async def hello(request):
return text("Answer")
@app.route('/', host="sub.example.com")
async def hello(request):
return text("42")
@bp.route("/question")
async def hello(request):
return text("What is the meaning of life?")
@bp.route("/answer")
async def hello(request):
return text("42")
app.register_blueprint(bp)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@ -12,3 +12,6 @@ kyoukai
falcon falcon
tornado tornado
aiofiles aiofiles
sphinx
recommonmark
beautifulsoup4

View File

@ -2,4 +2,3 @@ httptools
ujson ujson
uvloop uvloop
aiofiles aiofiles
multidict

View File

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

View File

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

View File

@ -3,6 +3,7 @@ from collections import defaultdict
class BlueprintSetup: class BlueprintSetup:
""" """
Creates a blueprint state like object.
""" """
def __init__(self, blueprint, app, options): def __init__(self, blueprint, app, options):
@ -18,24 +19,27 @@ class BlueprintSetup:
#: blueprint. #: blueprint.
self.url_prefix = url_prefix self.url_prefix = url_prefix
def add_route(self, handler, uri, methods): def add_route(self, handler, uri, methods, host=None):
""" """
A helper method to register a handler to the application url routes. A helper method to register a handler to the application url routes.
""" """
if self.url_prefix: if self.url_prefix:
uri = self.url_prefix + uri uri = self.url_prefix + uri
self.app.route(uri=uri, methods=methods)(handler) if host is None:
host = self.blueprint.host
self.app.route(uri=uri, methods=methods, host=host)(handler)
def add_exception(self, handler, *args, **kwargs): def add_exception(self, handler, *args, **kwargs):
""" """
Registers exceptions to sanic Registers exceptions to sanic.
""" """
self.app.exception(*args, **kwargs)(handler) self.app.exception(*args, **kwargs)(handler)
def add_static(self, uri, file_or_directory, *args, **kwargs): def add_static(self, uri, file_or_directory, *args, **kwargs):
""" """
Registers static files to sanic Registers static files to sanic.
""" """
if self.url_prefix: if self.url_prefix:
uri = self.url_prefix + uri uri = self.url_prefix + uri
@ -44,7 +48,7 @@ class BlueprintSetup:
def add_middleware(self, middleware, *args, **kwargs): def add_middleware(self, middleware, *args, **kwargs):
""" """
Registers middleware to sanic Registers middleware to sanic.
""" """
if args or kwargs: if args or kwargs:
self.app.middleware(*args, **kwargs)(middleware) self.app.middleware(*args, **kwargs)(middleware)
@ -53,7 +57,7 @@ class BlueprintSetup:
class Blueprint: class Blueprint:
def __init__(self, name, url_prefix=None): def __init__(self, name, url_prefix=None, host=None):
""" """
Creates a new blueprint Creates a new blueprint
:param name: Unique name of the blueprint :param name: Unique name of the blueprint
@ -63,6 +67,7 @@ class Blueprint:
self.url_prefix = url_prefix self.url_prefix = url_prefix
self.deferred_functions = [] self.deferred_functions = []
self.listeners = defaultdict(list) self.listeners = defaultdict(list)
self.host = host
def record(self, func): def record(self, func):
""" """
@ -73,32 +78,43 @@ class Blueprint:
def make_setup_state(self, app, options): def make_setup_state(self, app, options):
""" """
Returns a new BlueprintSetup object
""" """
return BlueprintSetup(self, app, options) return BlueprintSetup(self, app, options)
def register(self, app, options): def register(self, app, options):
""" """
Registers the blueprint to the sanic app.
""" """
state = self.make_setup_state(app, options) state = self.make_setup_state(app, options)
for deferred in self.deferred_functions: for deferred in self.deferred_functions:
deferred(state) deferred(state)
def route(self, uri, methods=None): def route(self, uri, methods=frozenset({'GET'}), host=None):
""" """
Creates 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): def decorator(handler):
self.record(lambda s: s.add_route(handler, uri, methods)) self.record(lambda s: s.add_route(handler, uri, methods, host))
return handler return handler
return decorator return decorator
def add_route(self, handler, uri, methods=None): def add_route(self, handler, uri, methods=None, host=None):
""" """
Creates a blueprint route from a function.
:param handler: Function to handle uri request.
:param uri: Endpoint at which the route will be accessible.
:param methods: List of acceptable HTTP methods.
""" """
self.record(lambda s: s.add_route(handler, uri, methods)) self.record(lambda s: s.add_route(handler, uri, methods, host))
return handler return handler
def listener(self, event): def listener(self, event):
""" """
Create a listener from a decorated function.
:param event: Event to listen to.
""" """
def decorator(listener): def decorator(listener):
self.listeners[event].append(listener) self.listeners[event].append(listener)
@ -107,6 +123,7 @@ class Blueprint:
def middleware(self, *args, **kwargs): def middleware(self, *args, **kwargs):
""" """
Creates a blueprint middleware from a decorated function.
""" """
def register_middleware(middleware): def register_middleware(middleware):
self.record( self.record(
@ -123,6 +140,7 @@ class Blueprint:
def exception(self, *args, **kwargs): def exception(self, *args, **kwargs):
""" """
Creates a blueprint exception from a decorated function.
""" """
def decorator(handler): def decorator(handler):
self.record(lambda s: s.add_exception(handler, *args, **kwargs)) self.record(lambda s: s.add_exception(handler, *args, **kwargs))
@ -131,6 +149,9 @@ class Blueprint:
def static(self, uri, file_or_directory, *args, **kwargs): def static(self, uri, file_or_directory, *args, **kwargs):
""" """
Creates a blueprint static route from a decorated function.
:param uri: Endpoint at which the route will be accessible.
:param file_or_directory: Static asset.
""" """
self.record( self.record(
lambda s: s.add_static(uri, file_or_directory, *args, **kwargs)) lambda s: s.add_static(uri, file_or_directory, *args, **kwargs))

View File

@ -1,5 +1,104 @@
from .response import text from .response import text, html
from traceback import format_exc from .log import log
from traceback import format_exc, extract_tb
import sys
TRACEBACK_STYLE = '''
<style>
body {
padding: 20px;
font-family: Arial, sans-serif;
}
p {
margin: 0;
}
.summary {
padding: 10px;
}
h1 {
margin-bottom: 0;
}
h3 {
margin-top: 10px;
}
h3 code {
font-size: 24px;
}
.frame-line > * {
padding: 5px 10px;
}
.frame-line {
margin-bottom: 5px;
}
.frame-code {
font-size: 16px;
padding-left: 30px;
}
.tb-wrapper {
border: 1px solid #f3f3f3;
}
.tb-header {
background-color: #f3f3f3;
padding: 5px 10px;
}
.frame-descriptor {
background-color: #e2eafb;
}
.frame-descriptor {
font-size: 14px;
}
</style>
'''
TRACEBACK_WRAPPER_HTML = '''
<html>
<head>
{style}
</head>
<body>
<h1>{exc_name}</h1>
<h3><code>{exc_value}</code></h3>
<div class="tb-wrapper">
<p class="tb-header">Traceback (most recent call last):</p>
{frame_html}
<p class="summary">
<b>{exc_name}: {exc_value}</b>
while handling uri <code>{uri}</code>
</p>
</div>
</body>
</html>
'''
TRACEBACK_LINE_HTML = '''
<div class="frame-line">
<p class="frame-descriptor">
File {0.filename}, line <i>{0.lineno}</i>,
in <code><b>{0.name}</b></code>
</p>
<p class="frame-code"><code>{0.line}</code></p>
</div>
'''
INTERNAL_SERVER_ERROR_HTML = '''
<h1>Internal Server Error</h1>
<p>
The server encountered an internal error and cannot complete
your request.
</p>
'''
class SanicException(Exception): class SanicException(Exception):
@ -45,29 +144,61 @@ class Handler:
self.handlers = {} self.handlers = {}
self.sanic = sanic self.sanic = sanic
def _render_traceback_html(self, exception, request):
exc_type, exc_value, tb = sys.exc_info()
frames = extract_tb(tb)
frame_html = []
for frame in frames:
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
return TRACEBACK_WRAPPER_HTML.format(
style=TRACEBACK_STYLE,
exc_name=exc_type.__name__,
exc_value=exc_value,
frame_html=''.join(frame_html),
uri=request.url)
def add(self, exception, handler): def add(self, exception, handler):
self.handlers[exception] = handler self.handlers[exception] = handler
def response(self, request, exception): def response(self, request, exception):
""" """
Fetches and executes an exception handler and returns a response object Fetches and executes an exception handler and returns a response object
:param request: Request :param request: Request
:param exception: Exception to handle :param exception: Exception to handle
:return: Response object :return: Response object
""" """
handler = self.handlers.get(type(exception), self.default) handler = self.handlers.get(type(exception), self.default)
response = handler(request=request, exception=exception) try:
response = handler(request=request, exception=exception)
except:
log.error(format_exc())
if self.sanic.debug:
response_message = (
'Exception raised in exception handler "{}" '
'for uri: "{}"\n{}').format(
handler.__name__, request.url, format_exc())
log.error(response_message)
return text(response_message, 500)
else:
return text('An error occurred while handling an error', 500)
return response return response
def default(self, request, exception): def default(self, request, exception):
log.error(format_exc())
if issubclass(type(exception), SanicException): if issubclass(type(exception), SanicException):
return text( return text(
"Error: {}".format(exception), 'Error: {}'.format(exception),
status=getattr(exception, 'status_code', 500)) status=getattr(exception, 'status_code', 500))
elif self.sanic.debug: elif self.sanic.debug:
return text( html_output = self._render_traceback_html(exception, request)
"Error: {}\nException: {}".format(
exception, format_exc()), status=500) response_message = (
'Exception occurred while handling uri: "{}"\n{}'.format(
request.url, format_exc()))
log.error(response_message)
return html(html_output, status=500)
else: else:
return text( return html(INTERNAL_SERVER_ERROR_HTML, status=500)
"An error occurred while generating the request", status=500)

View File

@ -1,3 +1,3 @@
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger('sanic')

View File

@ -21,16 +21,13 @@ class RequestParameters(dict):
value of the list and getlist returns the whole shebang value of the list and getlist returns the whole shebang
""" """
def __init__(self, *args, **kwargs):
self.super = super()
self.super.__init__(*args, **kwargs)
def get(self, name, default=None): def get(self, name, default=None):
values = self.super.get(name) """Return the first value, either the default or actual"""
return values[0] if values else default return super().get(name, [default])[0]
def getlist(self, name, default=None): def getlist(self, name, default=None):
return self.super.get(name, default) """Return the entire list"""
return super().get(name, default)
class Request(dict): class Request(dict):
@ -38,18 +35,20 @@ class Request(dict):
Properties of an HTTP request such as URL, headers, etc. Properties of an HTTP request such as URL, headers, etc.
""" """
__slots__ = ( __slots__ = (
'url', 'headers', 'version', 'method', '_cookies', 'url', 'headers', 'version', 'method', '_cookies', 'transport',
'query_string', 'body', 'query_string', 'body',
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
'_ip',
) )
def __init__(self, url_bytes, headers, version, method): def __init__(self, url_bytes, headers, version, method, transport):
# TODO: Content-Encoding detection # TODO: Content-Encoding detection
url_parsed = parse_url(url_bytes) url_parsed = parse_url(url_bytes)
self.url = url_parsed.path.decode('utf-8') self.url = url_parsed.path.decode('utf-8')
self.headers = headers self.headers = headers
self.version = version self.version = version
self.method = method self.method = method
self.transport = transport
self.query_string = None self.query_string = None
if url_parsed.query: if url_parsed.query:
self.query_string = url_parsed.query.decode('utf-8') self.query_string = url_parsed.query.decode('utf-8')
@ -64,7 +63,7 @@ class Request(dict):
@property @property
def json(self): def json(self):
if not self.parsed_json: if self.parsed_json is None:
try: try:
self.parsed_json = json_loads(self.body) self.parsed_json = json_loads(self.body)
except Exception: except Exception:
@ -72,6 +71,17 @@ class Request(dict):
return self.parsed_json return self.parsed_json
@property
def token(self):
"""
Attempts to return the auth header token.
:return: token related to request
"""
auth_header = self.headers.get('Authorization')
if auth_header is not None:
return auth_header.split()[1]
return auth_header
@property @property
def form(self): def form(self):
if self.parsed_form is None: if self.parsed_form is None:
@ -125,6 +135,12 @@ class Request(dict):
self._cookies = {} self._cookies = {}
return self._cookies return self._cookies
@property
def ip(self):
if not hasattr(self, '_ip'):
self._ip = self.transport.get_extra_info('peername')
return self._ip
File = namedtuple('File', ['type', 'body', 'name']) File = namedtuple('File', ['type', 'body', 'name'])
@ -132,6 +148,7 @@ File = namedtuple('File', ['type', 'body', 'name'])
def parse_multipart_form(body, boundary): def parse_multipart_form(body, boundary):
""" """
Parses a request body and returns fields and files Parses a request body and returns fields and files
:param body: Bytes request body :param body: Bytes request body
:param boundary: Bytes multipart boundary :param boundary: Bytes multipart boundary
:return: fields (RequestParameters), files (RequestParameters) :return: fields (RequestParameters), files (RequestParameters)

View File

@ -83,10 +83,10 @@ class HTTPResponse:
if body is not None: if body is not None:
try: try:
# Try to encode it regularly # Try to encode it regularly
self.body = body.encode('utf-8') self.body = body.encode()
except AttributeError: except AttributeError:
# Convert it to a str if you can't # Convert it to a str if you can't
self.body = str(body).encode('utf-8') self.body = str(body).encode()
else: else:
self.body = body_bytes self.body = body_bytes
@ -103,10 +103,14 @@ class HTTPResponse:
headers = b'' headers = b''
if self.headers: if self.headers:
headers = b''.join( for name, value in self.headers.items():
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) try:
for name, value in self.headers.items() headers += (
) b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')))
except AttributeError:
headers += (
b'%b: %b\r\n' % (
str(name).encode(), str(value).encode('utf-8')))
# Try to pull from the common codes first # Try to pull from the common codes first
# Speeds up response rate 6% over pulling from all # Speeds up response rate 6% over pulling from all
@ -139,21 +143,45 @@ class HTTPResponse:
def json(body, status=200, headers=None): def json(body, status=200, headers=None):
"""
Returns response object with body in json format.
:param body: Response data to be serialized.
:param status: Response code.
:param headers: Custom Headers.
"""
return HTTPResponse(json_dumps(body), headers=headers, status=status, return HTTPResponse(json_dumps(body), headers=headers, status=status,
content_type="application/json") content_type="application/json")
def text(body, status=200, headers=None): def text(body, status=200, headers=None):
"""
Returns response object with body in text format.
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
"""
return HTTPResponse(body, status=status, headers=headers, return HTTPResponse(body, status=status, headers=headers,
content_type="text/plain; charset=utf-8") content_type="text/plain; charset=utf-8")
def html(body, status=200, headers=None): def html(body, status=200, headers=None):
"""
Returns response object with body in html format.
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
"""
return HTTPResponse(body, status=status, headers=headers, return HTTPResponse(body, status=status, headers=headers,
content_type="text/html; charset=utf-8") content_type="text/html; charset=utf-8")
async def file(location, mime_type=None, headers=None): async def file(location, mime_type=None, headers=None):
"""
Returns response object with file data.
:param location: Location of file on system.
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
"""
filename = path.split(location)[-1] filename = path.split(location)[-1]
async with open_async(location, mode='rb') as _file: async with open_async(location, mode='rb') as _file:
@ -165,3 +193,26 @@ async def file(location, mime_type=None, headers=None):
headers=headers, headers=headers,
content_type=mime_type, content_type=mime_type,
body_bytes=out_stream) body_bytes=out_stream)
def redirect(to, headers=None, status=302,
content_type="text/html; charset=utf-8"):
"""
Aborts execution and causes a 302 redirect (by default).
:param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request
:param status: status code (int) of the new request, defaults to 302
:param content_type:
the content type (string) of the response
:returns: the redirecting Response
"""
headers = headers or {}
# According to RFC 7231, a relative URI is now permitted.
headers['Location'] = to
return HTTPResponse(
status=status,
headers=headers,
content_type=content_type)

View File

@ -3,6 +3,7 @@ from collections import defaultdict, namedtuple
from functools import lru_cache from functools import lru_cache
from .config import Config from .config import Config
from .exceptions import NotFound, InvalidUsage from .exceptions import NotFound, InvalidUsage
from .views import CompositionView
Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters']) Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters'])
Parameter = namedtuple('Parameter', ['name', 'cast']) Parameter = namedtuple('Parameter', ['name', 'cast'])
@ -23,16 +24,28 @@ class RouteExists(Exception):
pass pass
class RouteDoesNotExist(Exception):
pass
class Router: class Router:
""" """
Router supports basic routing with parameters and method checks Router supports basic routing with parameters and method checks
Usage: Usage:
@sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...])
def my_route(request, my_parameter): .. code-block:: python
@sanic.route('/my/url/<my_param>', methods=['GET', 'POST', ...])
def my_route(request, my_param):
do stuff... do stuff...
or or
@sanic.route('/my/url/<my_paramter>:type', methods['GET', 'POST', ...])
def my_route_with_type(request, my_parameter): .. code-block:: python
@sanic.route('/my/url/<my_param:my_type>', methods['GET', 'POST', ...])
def my_route_with_type(request, my_param: my_type):
do stuff... do stuff...
Parameters will be passed as keyword arguments to the request handling Parameters will be passed as keyword arguments to the request handling
@ -51,19 +64,36 @@ class Router:
self.routes_static = {} self.routes_static = {}
self.routes_dynamic = defaultdict(list) self.routes_dynamic = defaultdict(list)
self.routes_always_check = [] self.routes_always_check = []
self.hosts = None
def add(self, uri, methods, handler): def add(self, uri, methods, handler, host=None):
""" """
Adds a handler to the route list Adds a handler to the route list
:param uri: Path to match :param uri: Path to match
:param methods: Array of accepted method names. :param methods: Array of accepted method names.
If none are provided, any method is allowed If none are provided, any method is allowed
:param handler: Request handler function. :param handler: Request handler function.
When executed, it should provide a response object. When executed, it should provide a response object.
:return: Nothing :return: Nothing
""" """
if uri in self.routes_all:
raise RouteExists("Route already registered: {}".format(uri)) if host is not None:
# we want to track if there are any
# vhosts on the Router instance so that we can
# default to the behavior without vhosts
if self.hosts is None:
self.hosts = set(host)
else:
if isinstance(host, list):
host = set(host)
self.hosts.add(host)
if isinstance(host, str):
uri = host + uri
else:
for h in host:
self.add(uri, methods, handler, h)
return
# Dict for faster lookups of if method allowed # Dict for faster lookups of if method allowed
if methods: if methods:
@ -97,9 +127,35 @@ class Router:
pattern_string = re.sub(r'<(.+?)>', add_parameter, uri) pattern_string = re.sub(r'<(.+?)>', add_parameter, uri)
pattern = re.compile(r'^{}$'.format(pattern_string)) pattern = re.compile(r'^{}$'.format(pattern_string))
route = Route( def merge_route(route, methods, handler):
handler=handler, methods=methods, pattern=pattern, # merge to the existing route when possible.
parameters=parameters) if not route.methods or not methods:
# method-unspecified routes are not mergeable.
raise RouteExists(
"Route already registered: {}".format(uri))
elif route.methods.intersection(methods):
# already existing method is not overloadable.
duplicated = methods.intersection(route.methods)
raise RouteExists(
"Route already registered: {} [{}]".format(
uri, ','.join(list(duplicated))))
if isinstance(route.handler, CompositionView):
view = route.handler
else:
view = CompositionView()
view.add(route.methods, route.handler)
view.add(methods, handler)
route = route._replace(
handler=view, methods=methods.union(route.methods))
return route
route = self.routes_all.get(uri)
if route:
route = merge_route(route, methods, handler)
else:
route = Route(
handler=handler, methods=methods, pattern=pattern,
parameters=parameters)
self.routes_all[uri] = route self.routes_all[uri] = route
if properties['unhashable']: if properties['unhashable']:
@ -109,17 +165,41 @@ class Router:
else: else:
self.routes_static[uri] = route self.routes_static[uri] = route
def remove(self, uri, clean_cache=True, host=None):
if host is not None:
uri = host + uri
try:
route = self.routes_all.pop(uri)
except KeyError:
raise RouteDoesNotExist("Route was not registered: {}".format(uri))
if route in self.routes_always_check:
self.routes_always_check.remove(route)
elif url_hash(uri) in self.routes_dynamic \
and route in self.routes_dynamic[url_hash(uri)]:
self.routes_dynamic[url_hash(uri)].remove(route)
else:
self.routes_static.pop(uri)
if clean_cache:
self._get.cache_clear()
def get(self, request): def get(self, request):
""" """
Gets a request handler based on the URL of the request, or raises an Gets a request handler based on the URL of the request, or raises an
error error
:param request: Request object :param request: Request object
:return: handler, arguments, keyword arguments :return: handler, arguments, keyword arguments
""" """
return self._get(request.url, request.method) if self.hosts is None:
return self._get(request.url, request.method, '')
else:
return self._get(request.url, request.method,
request.headers.get("Host", ''))
@lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE)
def _get(self, url, method): def _get(self, url, method, host):
""" """
Gets a request handler based on the URL of the request, or raises an Gets a request handler based on the URL of the request, or raises an
error. Internal method for caching. error. Internal method for caching.
@ -127,6 +207,7 @@ class Router:
:param method: Request method :param method: Request method
:return: handler, arguments, keyword arguments :return: handler, arguments, keyword arguments
""" """
url = host + url
# Check against known static routes # Check against known static routes
route = self.routes_static.get(url) route = self.routes_static.get(url)
if route: if route:

View File

@ -4,7 +4,6 @@ from functools import partial
from inspect import isawaitable, stack, getmodulename from inspect import isawaitable, stack, getmodulename
from multiprocessing import Process, Event from multiprocessing import Process, Event
from signal import signal, SIGTERM, SIGINT from signal import signal, SIGTERM, SIGINT
from time import sleep
from traceback import format_exc from traceback import format_exc
import logging import logging
@ -13,19 +12,25 @@ from .exceptions import Handler
from .log import log from .log import log
from .response import HTTPResponse from .response import HTTPResponse
from .router import Router from .router import Router
from .server import serve from .server import serve, HttpProtocol
from .static import register as static_register from .static import register as static_register
from .exceptions import ServerError from .exceptions import ServerError
from socket import socket, SOL_SOCKET, SO_REUSEADDR
from os import set_inheritable
class Sanic: class Sanic:
def __init__(self, name=None, router=None, def __init__(self, name=None, router=None,
error_handler=None, logger=None): error_handler=None):
if logger is None: # Only set up a default log handler if the
logging.basicConfig( # end-user application didn't set anything up.
level=logging.INFO, if not logging.root.handlers and log.level == logging.NOTSET:
format="%(asctime)s: %(levelname)s: %(message)s" formatter = logging.Formatter(
) "%(asctime)s: %(levelname)s: %(message)s")
handler = logging.StreamHandler()
handler.setFormatter(formatter)
log.addHandler(handler)
log.setLevel(logging.INFO)
if name is None: if name is None:
frame_records = stack()[1] frame_records = stack()[1]
name = getmodulename(frame_records[1]) name = getmodulename(frame_records[1])
@ -39,6 +44,8 @@ class Sanic:
self._blueprint_order = [] self._blueprint_order = []
self.loop = None self.loop = None
self.debug = None self.debug = None
self.sock = None
self.processes = None
# Register alternative method names # Register alternative method names
self.go_fast = self.run self.go_fast = self.run
@ -48,9 +55,10 @@ class Sanic:
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# Decorator # Decorator
def route(self, uri, methods=None): def route(self, uri, methods=frozenset({'GET'}), host=None):
""" """
Decorates a function to be registered as a route Decorates a function to be registered as a route
:param uri: path of the URL :param uri: path of the URL
:param methods: list or tuple of methods allowed :param methods: list or tuple of methods allowed
:return: decorated function :return: decorated function
@ -62,29 +70,54 @@ class Sanic:
uri = '/' + uri uri = '/' + uri
def response(handler): def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler) self.router.add(uri=uri, methods=methods, handler=handler,
host=host)
return handler return handler
return response return response
def add_route(self, handler, uri, methods=None): # Shorthand method decorators
def get(self, uri, host=None):
return self.route(uri, methods=["GET"], host=host)
def post(self, uri, host=None):
return self.route(uri, methods=["POST"], host=host)
def put(self, uri, host=None):
return self.route(uri, methods=["PUT"], host=host)
def head(self, uri, host=None):
return self.route(uri, methods=["HEAD"], host=host)
def options(self, uri, host=None):
return self.route(uri, methods=["OPTIONS"], host=host)
def patch(self, uri, host=None):
return self.route(uri, methods=["PATCH"], host=host)
def add_route(self, handler, uri, methods=None, host=None):
""" """
A helper method to register class instance or A helper method to register class instance or
functions as a handler to the application url functions as a handler to the application url
routes. routes.
:param handler: function or class instance :param handler: function or class instance
:param uri: path of the URL :param uri: path of the URL
:param methods: list or tuple of methods allowed :param methods: list or tuple of methods allowed
:return: function or class instance :return: function or class instance
""" """
self.route(uri=uri, methods=methods)(handler) self.route(uri=uri, methods=methods, host=host)(handler)
return handler return handler
def remove_route(self, uri, clean_cache=True, host=None):
self.router.remove(uri, clean_cache, host)
# Decorator # Decorator
def exception(self, *exceptions): def exception(self, *exceptions):
""" """
Decorates a function to be registered as a handler for exceptions Decorates a function to be registered as a handler for exceptions
:param *exceptions: exceptions
:param \*exceptions: exceptions
:return: decorated function :return: decorated function
""" """
@ -130,6 +163,7 @@ class Sanic:
def blueprint(self, blueprint, **options): def blueprint(self, blueprint, **options):
""" """
Registers a blueprint on the application. Registers a blueprint on the application.
:param blueprint: Blueprint object :param blueprint: Blueprint object
:param options: option dictionary with blueprint defaults :param options: option dictionary with blueprint defaults
:return: Nothing :return: Nothing
@ -147,7 +181,8 @@ class Sanic:
def register_blueprint(self, *args, **kwargs): def register_blueprint(self, *args, **kwargs):
# TODO: deprecate 1.0 # TODO: deprecate 1.0
log.warning("Use of register_blueprint will be deprecated in " log.warning("Use of register_blueprint will be deprecated in "
"version 1.0. Please use the blueprint method instead") "version 1.0. Please use the blueprint method instead",
DeprecationWarning)
return self.blueprint(*args, **kwargs) return self.blueprint(*args, **kwargs)
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
@ -162,9 +197,10 @@ class Sanic:
Takes a request from the HTTP Server and returns a response object to Takes a request from the HTTP Server and returns a response object to
be sent back The HTTP Server only expects a response object, so be sent back The HTTP Server only expects a response object, so
exception handling must be done here exception handling must be done here
:param request: HTTP Request object :param request: HTTP Request object
:param response_callback: Response function to be called with the :param response_callback: Response function to be called with the
response as the only argument response as the only argument
:return: Nothing :return: Nothing
""" """
try: try:
@ -229,7 +265,7 @@ class Sanic:
e, format_exc())) e, format_exc()))
else: else:
response = HTTPResponse( response = HTTPResponse(
"An error occured while handling an error") "An error occurred while handling an error")
response_callback(response) response_callback(response)
@ -238,26 +274,30 @@ class Sanic:
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
after_start=None, before_stop=None, after_stop=None, sock=None, after_start=None, before_stop=None, after_stop=None, ssl=None,
workers=1, loop=None, register_sys_signals=True): sock=None, workers=1, loop=None, protocol=HttpProtocol,
backlog=100, stop_event=None, register_sys_signals=True):
""" """
Runs the HTTP Server and listens until keyboard interrupt or term Runs the HTTP Server and listens until keyboard interrupt or term
signal. On termination, drains connections before closing. signal. On termination, drains connections before closing.
:param host: Address to host on :param host: Address to host on
:param port: Port to host on :param port: Port to host on
:param debug: Enables debug output (slows server) :param debug: Enables debug output (slows server)
:param before_start: Function to be executed before the server starts :param before_start: Functions to be executed before the server starts
accepting connections accepting connections
:param after_start: Function to be executed after the server starts :param after_start: Functions to be executed after the server starts
accepting connections accepting connections
:param before_stop: Function to be executed when a stop signal is :param before_stop: Functions to be executed when a stop signal is
received before it is respected received before it is respected
:param after_stop: Function to be executed when all requests are :param after_stop: Functions to be executed when all requests are
complete complete
:param ssl: SSLContext for SSL encryption of worker(s)
:param sock: Socket for the server to accept connections from :param sock: Socket for the server to accept connections from
:param workers: Number of processes :param workers: Number of processes
received before it is respected received before it is respected
:param loop: asyncio compatible event loop :param loop: asyncio compatible event loop
:param protocol: Subclass of asyncio protocol class
:return: Nothing :return: Nothing
""" """
self.error_handler.debug = True self.error_handler.debug = True
@ -265,16 +305,19 @@ class Sanic:
self.loop = loop self.loop = loop
server_settings = { server_settings = {
'protocol': protocol,
'host': host, 'host': host,
'port': port, 'port': port,
'sock': sock, 'sock': sock,
'ssl': ssl,
'debug': debug, 'debug': debug,
'request_handler': self.handle_request, 'request_handler': self.handle_request,
'error_handler': self.error_handler, 'error_handler': self.error_handler,
'request_timeout': self.config.REQUEST_TIMEOUT, 'request_timeout': self.config.REQUEST_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE, 'request_max_size': self.config.REQUEST_MAX_SIZE,
'loop': loop, 'loop': loop,
'register_sys_signals': register_sys_signals 'register_sys_signals': register_sys_signals,
'backlog': backlog
} }
# -------------------------------------------- # # -------------------------------------------- #
@ -291,7 +334,7 @@ class Sanic:
for blueprint in self.blueprints.values(): for blueprint in self.blueprints.values():
listeners += blueprint.listeners[event_name] listeners += blueprint.listeners[event_name]
if args: if args:
if type(args) is not list: if callable(args):
args = [args] args = [args]
listeners += args listeners += args
if reverse: if reverse:
@ -305,7 +348,11 @@ class Sanic:
log.debug(self.config.LOGO) log.debug(self.config.LOGO)
# Serve # Serve
log.info('Goin\' Fast @ http://{}:{}'.format(host, port)) if ssl is None:
proto = "http"
else:
proto = "https"
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
try: try:
if workers == 1: if workers == 1:
@ -313,7 +360,7 @@ class Sanic:
else: else:
log.info('Spinning up {} workers...'.format(workers)) log.info('Spinning up {} workers...'.format(workers))
self.serve_multiple(server_settings, workers) self.serve_multiple(server_settings, workers, stop_event)
except Exception as e: except Exception as e:
log.exception( log.exception(
@ -325,13 +372,17 @@ class Sanic:
""" """
This kills the Sanic This kills the Sanic
""" """
if self.processes is not None:
for process in self.processes:
process.terminate()
self.sock.close()
get_event_loop().stop() get_event_loop().stop()
@staticmethod def serve_multiple(self, server_settings, workers, stop_event=None):
def serve_multiple(server_settings, workers, stop_event=None):
""" """
Starts multiple server processes simultaneously. Stops on interrupt Starts multiple server processes simultaneously. Stops on interrupt
and terminate signals, and drains connections when complete. and terminate signals, and drains connections when complete.
:param server_settings: kw arguments to be passed to the serve function :param server_settings: kw arguments to be passed to the serve function
:param workers: number of workers to launch :param workers: number of workers to launch
:param stop_event: if provided, is used as a stop signal :param stop_event: if provided, is used as a stop signal
@ -340,26 +391,28 @@ class Sanic:
server_settings['reuse_port'] = True server_settings['reuse_port'] = True
# Create a stop event to be triggered by a signal # Create a stop event to be triggered by a signal
if not stop_event: if stop_event is None:
stop_event = Event() stop_event = Event()
signal(SIGINT, lambda s, f: stop_event.set()) signal(SIGINT, lambda s, f: stop_event.set())
signal(SIGTERM, lambda s, f: stop_event.set()) signal(SIGTERM, lambda s, f: stop_event.set())
processes = [] self.sock = socket()
self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
self.sock.bind((server_settings['host'], server_settings['port']))
set_inheritable(self.sock.fileno(), True)
server_settings['sock'] = self.sock
server_settings['host'] = None
server_settings['port'] = None
self.processes = []
for _ in range(workers): for _ in range(workers):
process = Process(target=serve, kwargs=server_settings) process = Process(target=serve, kwargs=server_settings)
process.daemon = True
process.start() process.start()
processes.append(process) self.processes.append(process)
# Infinitely wait for the stop event for process in self.processes:
try:
while not stop_event.is_set():
sleep(0.3)
except:
pass
log.info('Spinning down workers...')
for process in processes:
process.terminate()
for process in processes:
process.join() process.join()
# the above processes will block this until they're stopped
self.stop()

View File

@ -1,7 +1,7 @@
import asyncio import asyncio
import traceback
from functools import partial from functools import partial
from inspect import isawaitable from inspect import isawaitable
from multidict import CIMultiDict
from signal import SIGINT, SIGTERM from signal import SIGINT, SIGTERM
from time import time from time import time
from httptools import HttpRequestParser from httptools import HttpRequestParser
@ -18,11 +18,30 @@ from .request import Request
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
current_time = None
class Signal: class Signal:
stopped = False stopped = False
current_time = None class CIDict(dict):
"""
Case Insensitive dict where all keys are converted to lowercase
This does not maintain the inputted case when calling items() or keys()
in favor of speed, since headers are case insensitive
"""
def get(self, key, default=None):
return super().get(key.casefold(), default)
def __getitem__(self, key):
return super().__getitem__(key.casefold())
def __setitem__(self, key, value):
return super().__setitem__(key.casefold(), value)
def __contains__(self, key):
return super().__contains__(key.casefold())
class HttpProtocol(asyncio.Protocol): class HttpProtocol(asyncio.Protocol):
@ -70,15 +89,14 @@ class HttpProtocol(asyncio.Protocol):
def connection_lost(self, exc): def connection_lost(self, exc):
self.connections.discard(self) self.connections.discard(self)
self._timeout_handler.cancel() self._timeout_handler.cancel()
self.cleanup()
def connection_timeout(self): def connection_timeout(self):
# Check if # Check if
time_elapsed = current_time - self._last_request_time time_elapsed = current_time - self._last_request_time
if time_elapsed < self.request_timeout: if time_elapsed < self.request_timeout:
time_left = self.request_timeout - time_elapsed time_left = self.request_timeout - time_elapsed
self._timeout_handler = \ self._timeout_handler = (
self.loop.call_later(time_left, self.connection_timeout) self.loop.call_later(time_left, self.connection_timeout))
else: else:
if self._request_handler_task: if self._request_handler_task:
self._request_handler_task.cancel() self._request_handler_task.cancel()
@ -118,18 +136,15 @@ class HttpProtocol(asyncio.Protocol):
exception = PayloadTooLarge('Payload Too Large') exception = PayloadTooLarge('Payload Too Large')
self.write_error(exception) self.write_error(exception)
self.headers.append((name.decode(), value.decode('utf-8'))) self.headers.append((name.decode().casefold(), value.decode()))
def on_headers_complete(self): def on_headers_complete(self):
remote_addr = self.transport.get_extra_info('peername')
if remote_addr:
self.headers.append(('Remote-Addr', '%s:%s' % remote_addr))
self.request = Request( self.request = Request(
url_bytes=self.url, url_bytes=self.url,
headers=CIMultiDict(self.headers), headers=CIDict(self.headers),
version=self.parser.get_http_version(), version=self.parser.get_http_version(),
method=self.parser.get_method().decode() method=self.parser.get_method().decode(),
transport=self.transport
) )
def on_body(self, body): def on_body(self, body):
@ -148,35 +163,54 @@ class HttpProtocol(asyncio.Protocol):
def write_response(self, response): def write_response(self, response):
try: try:
keep_alive = self.parser.should_keep_alive() \ keep_alive = (
and not self.signal.stopped self.parser.should_keep_alive() and not self.signal.stopped)
self.transport.write( self.transport.write(
response.output( response.output(
self.request.version, keep_alive, self.request_timeout)) self.request.version, keep_alive, self.request_timeout))
except RuntimeError:
log.error(
'Connection lost before response written @ {}'.format(
self.request.ip))
except Exception as e:
self.bail_out(
"Writing response failed, connection closed {}".format(e))
finally:
if not keep_alive: if not keep_alive:
self.transport.close() self.transport.close()
else: else:
# Record that we received data # Record that we received data
self._last_request_time = current_time self._last_request_time = current_time
self.cleanup() self.cleanup()
except Exception as e:
self.bail_out(
"Writing response failed, connection closed {}".format(e))
def write_error(self, exception): def write_error(self, exception):
try: try:
response = self.error_handler.response(self.request, exception) response = self.error_handler.response(self.request, exception)
version = self.request.version if self.request else '1.1' version = self.request.version if self.request else '1.1'
self.transport.write(response.output(version)) self.transport.write(response.output(version))
self.transport.close() except RuntimeError:
log.error(
'Connection lost before error written @ {}'.format(
self.request.ip))
except Exception as e: except Exception as e:
self.bail_out( self.bail_out(
"Writing error failed, connection closed {}".format(e)) "Writing error failed, connection closed {}".format(e),
from_error=True)
finally:
self.transport.close()
def bail_out(self, message): def bail_out(self, message, from_error=False):
exception = ServerError(message) if from_error and self.transport.is_closing():
self.write_error(exception) log.error(
log.error(message) ("Transport closed @ {} and exception "
"experienced during error handling").format(
self.transport.get_extra_info('peername')))
log.debug(
'Exception:\n{}'.format(traceback.format_exc()))
else:
exception = ServerError(message)
self.write_error(exception)
log.error(message)
def cleanup(self): def cleanup(self):
self.parser = None self.parser = None
@ -201,6 +235,7 @@ def update_current_time(loop):
""" """
Caches the current time, since it is needed Caches the current time, since it is needed
at the end of every keep-alive request to update the request timeout time at the end of every keep-alive request to update the request timeout time
:param loop: :param loop:
:return: :return:
""" """
@ -224,25 +259,35 @@ def trigger_events(events, loop):
def serve(host, port, request_handler, error_handler, before_start=None, def serve(host, port, request_handler, error_handler, before_start=None,
after_start=None, before_stop=None, after_stop=None, after_start=None, before_stop=None, after_stop=None, debug=False,
debug=False, request_timeout=60, sock=None, request_timeout=60, ssl=None, sock=None, request_max_size=None,
request_max_size=None, reuse_port=False, loop=None, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
register_sys_signals=True): register_sys_signals=True):
""" """
Starts asynchronous HTTP Server on an individual process. Starts asynchronous HTTP Server on an individual process.
:param host: Address to host on :param host: Address to host on
:param port: Port to host on :param port: Port to host on
:param request_handler: Sanic request handler with middleware :param request_handler: Sanic request handler with middleware
:param error_handler: Sanic error handler with middleware
:param before_start: Function to be executed before the server starts
listening. Takes single argument `loop`
:param after_start: Function to be executed after the server starts :param after_start: Function to be executed after the server starts
listening. Takes single argument `loop` listening. Takes single argument `loop`
:param before_stop: Function to be executed when a stop signal is :param before_stop: Function to be executed when a stop signal is
received before it is respected. Takes single argumenet `loop` received before it is respected. Takes single
argument `loop`
:param after_stop: Function to be executed when a stop signal is
received after it is respected. Takes single
argument `loop`
:param debug: Enables debug output (slows server) :param debug: Enables debug output (slows server)
:param request_timeout: time in seconds :param request_timeout: time in seconds
:param ssl: SSLContext
:param sock: Socket for the server to accept connections from :param sock: Socket for the server to accept connections from
:param request_max_size: size in bytes, `None` for no limit :param request_max_size: size in bytes, `None` for no limit
:param reuse_port: `True` for multiple workers :param reuse_port: `True` for multiple workers
:param loop: asyncio compatible event loop :param loop: asyncio compatible event loop
:param protocol: Subclass of asyncio protocol class
:return: Nothing :return: Nothing
""" """
loop = loop or async_loop.new_event_loop() loop = loop or async_loop.new_event_loop()
@ -256,7 +301,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
connections = set() connections = set()
signal = Signal() signal = Signal()
server = partial( server = partial(
HttpProtocol, protocol,
loop=loop, loop=loop,
connections=connections, connections=connections,
signal=signal, signal=signal,
@ -270,8 +315,10 @@ def serve(host, port, request_handler, error_handler, before_start=None,
server, server,
host, host,
port, port,
ssl=ssl,
reuse_port=reuse_port, reuse_port=reuse_port,
sock=sock sock=sock,
backlog=backlog
) )
# Instead of pulling time at the end of every request, # Instead of pulling time at the end of every request,

View File

@ -15,12 +15,14 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
""" """
Registers a static directory handler with Sanic by adding a route to the Registers a static directory handler with Sanic by adding a route to the
router and registering a handler. router and registering a handler.
:param app: Sanic :param app: Sanic
:param file_or_directory: File or directory path to serve from :param file_or_directory: File or directory path to serve from
:param uri: URL to serve from :param uri: URL to serve from
:param pattern: regular expression used to match files in the URL :param pattern: regular expression used to match files in the URL
:param use_modified_since: If true, send file modified time, and return :param use_modified_since: If true, send file modified time, and return
not modified if the browser's matches the server's not modified if the browser's matches the
server's
""" """
# If we're not trying to match a file directly, # If we're not trying to match a file directly,

View File

@ -16,15 +16,15 @@ async def local_request(method, uri, cookies=None, *args, **kwargs):
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
loop=None, debug=False, *request_args, loop=None, debug=False, server_kwargs={},
**request_kwargs): *request_args, **request_kwargs):
results = [] results = []
exceptions = [] exceptions = []
if gather_request: if gather_request:
@app.middleware
def _collect_request(request): def _collect_request(request):
results.append(request) results.append(request)
app.request_middleware.appendleft(_collect_request)
async def _collect_response(sanic, loop): async def _collect_response(sanic, loop):
try: try:
@ -35,8 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
exceptions.append(e) exceptions.append(e)
app.stop() app.stop()
app.run(host=HOST, debug=debug, port=42101, app.run(host=HOST, debug=debug, port=PORT,
after_start=_collect_response, loop=loop) after_start=_collect_response, loop=loop, **server_kwargs)
if exceptions: if exceptions:
raise ValueError("Exception during request: {}".format(exceptions)) raise ValueError("Exception during request: {}".format(exceptions))

View File

@ -7,33 +7,96 @@ class HTTPMethodView:
to every HTTP method you want to support. to every HTTP method you want to support.
For example: For example:
class DummyView(View):
.. code-block:: python
class DummyView(HTTPMethodView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return text('I am get method') return text('I am get method')
def put(self, request, *args, **kwargs): def put(self, request, *args, **kwargs):
return text('I am put method') return text('I am put method')
etc. etc.
If someone tries to use a non-implemented method, there will be a If someone tries to use a non-implemented method, there will be a
405 response. 405 response.
If you need any url params just mention them in method definition: If you need any url params just mention them in method definition:
class DummyView(View):
.. code-block:: python
class DummyView(HTTPMethodView):
def get(self, request, my_param_here, *args, **kwargs): def get(self, request, my_param_here, *args, **kwargs):
return text('I am get method with %s' % my_param_here) return text('I am get method with %s' % my_param_here)
To add the view into the routing you could use To add the view into the routing you could use
1) app.add_route(DummyView(), '/') 1) app.add_route(DummyView.as_view(), '/')
2) app.route('/')(DummyView()) 2) app.route('/')(DummyView.as_view())
To add any decorator you could set it into decorators variable
""" """
def __call__(self, request, *args, **kwargs): decorators = []
def dispatch_request(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None) handler = getattr(self, request.method.lower(), None)
if handler: if handler:
return handler(request, *args, **kwargs) return handler(request, *args, **kwargs)
raise InvalidUsage( raise InvalidUsage(
'Method {} not allowed for URL {}'.format( 'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405) request.method, request.url), status_code=405)
@classmethod
def as_view(cls, *class_args, **class_kwargs):
""" Converts the class into an actual view function that can be used
with the routing system.
"""
def view(*args, **kwargs):
self = view.view_class(*class_args, **class_kwargs)
return self.dispatch_request(*args, **kwargs)
if cls.decorators:
view.__module__ = cls.__module__
for decorator in cls.decorators:
view = decorator(view)
view.view_class = cls
view.__doc__ = cls.__doc__
view.__module__ = cls.__module__
return view
class CompositionView:
""" Simple method-function mapped view for the sanic.
You can add handler functions to methods (get, post, put, patch, delete)
for every HTTP method you want to support.
For example:
view = CompositionView()
view.add(['GET'], lambda request: text('I am get method'))
view.add(['POST', 'PUT'], lambda request: text('I am post/put method'))
etc.
If someone tries to use a non-implemented method, there will be a
405 response.
"""
def __init__(self):
self.handlers = {}
def add(self, methods, handler):
for method in methods:
if method in self.handlers:
raise KeyError(
'Method {} already is registered.'.format(method))
self.handlers[method] = handler
def __call__(self, request, *args, **kwargs):
handler = self.handlers.get(request.method.upper(), None)
if handler is None:
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405)
return handler(request, *args, **kwargs)

View File

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

View File

@ -59,6 +59,71 @@ def test_several_bp_with_url_prefix():
request, response = sanic_endpoint_test(app, uri='/test2/') request, response = sanic_endpoint_test(app, uri='/test2/')
assert response.text == 'Hello2' assert response.text == 'Hello2'
def test_bp_with_host():
app = Sanic('test_bp_host')
bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com")
@bp.route('/')
def handler(request):
return text('Hello')
@bp.route('/', host="sub.example.com")
def handler(request):
return text('Hello subdomain!')
app.blueprint(bp)
headers = {"Host": "example.com"}
request, response = sanic_endpoint_test(app, uri='/test1/',
headers=headers)
assert response.text == 'Hello'
headers = {"Host": "sub.example.com"}
request, response = sanic_endpoint_test(app, uri='/test1/',
headers=headers)
assert response.text == 'Hello subdomain!'
def test_several_bp_with_host():
app = Sanic('test_text')
bp = Blueprint('test_text',
url_prefix='/test',
host="example.com")
bp2 = Blueprint('test_text2',
url_prefix='/test',
host="sub.example.com")
@bp.route('/')
def handler(request):
return text('Hello')
@bp2.route('/')
def handler2(request):
return text('Hello2')
@bp2.route('/other/')
def handler2(request):
return text('Hello3')
app.blueprint(bp)
app.blueprint(bp2)
assert bp.host == "example.com"
headers = {"Host": "example.com"}
request, response = sanic_endpoint_test(app, uri='/test/',
headers=headers)
assert response.text == 'Hello'
assert bp2.host == "sub.example.com"
headers = {"Host": "sub.example.com"}
request, response = sanic_endpoint_test(app, uri='/test/',
headers=headers)
assert response.text == 'Hello2'
request, response = sanic_endpoint_test(app, uri='/test/other/',
headers=headers)
assert response.text == 'Hello3'
def test_bp_middleware(): def test_bp_middleware():
app = Sanic('test_middleware') app = Sanic('test_middleware')

View File

@ -0,0 +1,32 @@
from sanic import Sanic
from sanic.server import HttpProtocol
from sanic.response import text
from sanic.utils import sanic_endpoint_test
app = Sanic('test_custom_porotocol')
class CustomHttpProtocol(HttpProtocol):
def write_response(self, response):
if isinstance(response, str):
response = text(response)
self.transport.write(
response.output(self.request.version)
)
self.transport.close()
@app.route('/1')
async def handler_1(request):
return 'OK'
def test_use_custom_protocol():
server_kwargs = {
'protocol': CustomHttpProtocol
}
request, response = sanic_endpoint_test(app, uri='/1',
server_kwargs=server_kwargs)
assert response.status == 200
assert response.text == 'OK'

View File

@ -1,51 +1,93 @@
import pytest
from bs4 import BeautifulSoup
from sanic import Sanic from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.exceptions import InvalidUsage, ServerError, NotFound from sanic.exceptions import InvalidUsage, ServerError, NotFound
from sanic.utils import sanic_endpoint_test from sanic.utils import sanic_endpoint_test
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
exception_app = Sanic('test_exceptions') class SanicExceptionTestException(Exception):
pass
@exception_app.route('/') @pytest.fixture(scope='module')
def handler(request): def exception_app():
return text('OK') app = Sanic('test_exceptions')
@app.route('/')
def handler(request):
return text('OK')
@app.route('/error')
def handler_error(request):
raise ServerError("OK")
@app.route('/404')
def handler_404(request):
raise NotFound("OK")
@app.route('/invalid')
def handler_invalid(request):
raise InvalidUsage("OK")
@app.route('/divide_by_zero')
def handle_unhandled_exception(request):
1 / 0
@app.route('/error_in_error_handler_handler')
def custom_error_handler(request):
raise SanicExceptionTestException('Dummy message!')
@app.exception(SanicExceptionTestException)
def error_in_error_handler_handler(request, exception):
1 / 0
return app
@exception_app.route('/error') def test_no_exception(exception_app):
def handler_error(request): """Test that a route works without an exception"""
raise ServerError("OK")
@exception_app.route('/404')
def handler_404(request):
raise NotFound("OK")
@exception_app.route('/invalid')
def handler_invalid(request):
raise InvalidUsage("OK")
def test_no_exception():
request, response = sanic_endpoint_test(exception_app) request, response = sanic_endpoint_test(exception_app)
assert response.status == 200 assert response.status == 200
assert response.text == 'OK' assert response.text == 'OK'
def test_server_error_exception(): def test_server_error_exception(exception_app):
"""Test the built-in ServerError exception works"""
request, response = sanic_endpoint_test(exception_app, uri='/error') request, response = sanic_endpoint_test(exception_app, uri='/error')
assert response.status == 500 assert response.status == 500
def test_invalid_usage_exception(): def test_invalid_usage_exception(exception_app):
"""Test the built-in InvalidUsage exception works"""
request, response = sanic_endpoint_test(exception_app, uri='/invalid') request, response = sanic_endpoint_test(exception_app, uri='/invalid')
assert response.status == 400 assert response.status == 400
def test_not_found_exception(): def test_not_found_exception(exception_app):
"""Test the built-in NotFound exception works"""
request, response = sanic_endpoint_test(exception_app, uri='/404') request, response = sanic_endpoint_test(exception_app, uri='/404')
assert response.status == 404 assert response.status == 404
def test_handled_unhandled_exception(exception_app):
"""Test that an exception not built into sanic is handled"""
request, response = sanic_endpoint_test(
exception_app, uri='/divide_by_zero')
assert response.status == 500
soup = BeautifulSoup(response.body, 'html.parser')
assert soup.h1.text == 'Internal Server Error'
message = " ".join(soup.p.text.split())
assert message == (
"The server encountered an internal error and "
"cannot complete your request.")
def test_exception_in_exception_handler(exception_app):
"""Test that an exception thrown in an error handler is handled"""
request, response = sanic_endpoint_test(
exception_app, uri='/error_in_error_handler_handler')
assert response.status == 500
assert response.body == b'An error occurred while handling an error'

View File

@ -2,6 +2,7 @@ from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.exceptions import InvalidUsage, ServerError, NotFound from sanic.exceptions import InvalidUsage, ServerError, NotFound
from sanic.utils import sanic_endpoint_test from sanic.utils import sanic_endpoint_test
from bs4 import BeautifulSoup
exception_handler_app = Sanic('test_exception_handler') exception_handler_app = Sanic('test_exception_handler')
@ -21,6 +22,12 @@ def handler_3(request):
raise NotFound("OK") raise NotFound("OK")
@exception_handler_app.route('/4')
def handler_4(request):
foo = bar
return text(foo)
@exception_handler_app.exception(NotFound, ServerError) @exception_handler_app.exception(NotFound, ServerError)
def handler_exception(request, exception): def handler_exception(request, exception):
return text("OK") return text("OK")
@ -47,3 +54,20 @@ def test_text_exception__handler():
exception_handler_app, uri='/random') exception_handler_app, uri='/random')
assert response.status == 200 assert response.status == 200
assert response.text == 'OK' assert response.text == 'OK'
def test_html_traceback_output_in_debug_mode():
request, response = sanic_endpoint_test(
exception_handler_app, uri='/4', debug=True)
assert response.status == 500
soup = BeautifulSoup(response.body, 'html.parser')
html = str(soup)
assert 'response = handler(request, *args, **kwargs)' in html
assert 'handler_4' in html
assert 'foo = bar' in html
summary_text = " ".join(soup.select('.summary')[0].text.split())
assert (
"NameError: name 'bar' "
"is not defined while handling uri /4") == summary_text

View File

@ -19,7 +19,7 @@ def test_log():
stream=log_stream stream=log_stream
) )
log = logging.getLogger() log = logging.getLogger()
app = Sanic('test_logging', logger=True) app = Sanic('test_logging')
@app.route('/') @app.route('/')
def handler(request): def handler(request):
log.info('hello world') log.info('hello world')

View File

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

View File

@ -1,9 +1,10 @@
from json import loads as json_loads, dumps as json_dumps from json import loads as json_loads, dumps as json_dumps
from sanic import Sanic from sanic import Sanic
from sanic.response import json, text from sanic.response import json, text, redirect
from sanic.utils import sanic_endpoint_test from sanic.utils import sanic_endpoint_test
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
import pytest
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
# GET # GET
@ -33,6 +34,31 @@ def test_text():
assert response.text == 'Hello' assert response.text == 'Hello'
def test_headers():
app = Sanic('test_text')
@app.route('/')
async def handler(request):
headers = {"spam": "great"}
return text('Hello', headers=headers)
request, response = sanic_endpoint_test(app)
assert response.headers.get('spam') == 'great'
def test_non_str_headers():
app = Sanic('test_text')
@app.route('/')
async def handler(request):
headers = {"answer": 42}
return text('Hello', headers=headers)
request, response = sanic_endpoint_test(app)
assert response.headers.get('answer') == '42'
def test_invalid_response(): def test_invalid_response():
app = Sanic('test_invalid_response') app = Sanic('test_invalid_response')
@ -92,6 +118,24 @@ def test_query_string():
assert request.args.get('test2') == 'false' assert request.args.get('test2') == 'false'
def test_token():
app = Sanic('test_post_token')
@app.route('/')
async def handler(request):
return text('OK')
# uuid4 generated token.
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
headers = {
'content-type': 'application/json',
'Authorization': 'Token {}'.format(token)
}
request, response = sanic_endpoint_test(app, headers=headers)
assert request.token == token
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
# POST # POST
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
@ -145,3 +189,73 @@ def test_post_form_multipart_form_data():
request, response = sanic_endpoint_test(app, data=payload, headers=headers) request, response = sanic_endpoint_test(app, data=payload, headers=headers)
assert request.form.get('test') == 'OK' assert request.form.get('test') == 'OK'
@pytest.fixture
def redirect_app():
app = Sanic('test_redirection')
@app.route('/redirect_init')
async def redirect_init(request):
return redirect("/redirect_target")
@app.route('/redirect_init_with_301')
async def redirect_init_with_301(request):
return redirect("/redirect_target", status=301)
@app.route('/redirect_target')
async def redirect_target(request):
return text('OK')
return app
def test_redirect_default_302(redirect_app):
"""
We expect a 302 default status code and the headers to be set.
"""
request, response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init",
allow_redirects=False)
assert response.status == 302
assert response.headers["Location"] == "/redirect_target"
assert response.headers["Content-Type"] == 'text/html; charset=utf-8'
def test_redirect_headers_none(redirect_app):
request, response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init",
headers=None,
allow_redirects=False)
assert response.status == 302
assert response.headers["Location"] == "/redirect_target"
def test_redirect_with_301(redirect_app):
"""
Test redirection with a different status code.
"""
request, response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init_with_301",
allow_redirects=False)
assert response.status == 301
assert response.headers["Location"] == "/redirect_target"
def test_get_then_redirect_follow_redirect(redirect_app):
"""
With `allow_redirects` we expect a 200.
"""
response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init", gather_request=False,
allow_redirects=True)
assert response.status == 200
assert response.text == 'OK'

View File

@ -2,7 +2,7 @@ import pytest
from sanic import Sanic from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.router import RouteExists from sanic.router import RouteExists, RouteDoesNotExist
from sanic.utils import sanic_endpoint_test from sanic.utils import sanic_endpoint_test
@ -10,6 +10,84 @@ from sanic.utils import sanic_endpoint_test
# UTF-8 # UTF-8
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
def test_shorthand_routes_get():
app = Sanic('test_shorhand_routes_get')
@app.get('/get')
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/get', method='get')
assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/get', method='post')
assert response.status == 405
def test_shorthand_routes_post():
app = Sanic('test_shorhand_routes_post')
@app.post('/post')
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/post', method='post')
assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/post', method='get')
assert response.status == 405
def test_shorthand_routes_put():
app = Sanic('test_shorhand_routes_put')
@app.put('/put')
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/put', method='put')
assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/put', method='get')
assert response.status == 405
def test_shorthand_routes_patch():
app = Sanic('test_shorhand_routes_patch')
@app.patch('/patch')
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/patch', method='patch')
assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/patch', method='get')
assert response.status == 405
def test_shorthand_routes_head():
app = Sanic('test_shorhand_routes_head')
@app.head('/head')
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/head', method='head')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/head', method='get')
assert response.status == 405
def test_shorthand_routes_options():
app = Sanic('test_shorhand_routes_options')
@app.options('/options')
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/options', method='options')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/options', method='get')
assert response.status == 405
def test_static_routes(): def test_static_routes():
app = Sanic('test_dynamic_route') app = Sanic('test_dynamic_route')
@ -356,3 +434,174 @@ def test_add_route_method_not_allowed():
request, response = sanic_endpoint_test(app, method='post', uri='/test') request, response = sanic_endpoint_test(app, method='post', uri='/test')
assert response.status == 405 assert response.status == 405
def test_remove_static_route():
app = Sanic('test_remove_static_route')
async def handler1(request):
return text('OK1')
async def handler2(request):
return text('OK2')
app.add_route(handler1, '/test')
app.add_route(handler2, '/test2')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/test2')
assert response.status == 200
app.remove_route('/test')
app.remove_route('/test2')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/test2')
assert response.status == 404
def test_remove_dynamic_route():
app = Sanic('test_remove_dynamic_route')
async def handler(request, name):
return text('OK')
app.add_route(handler, '/folder/<name>')
request, response = sanic_endpoint_test(app, uri='/folder/test123')
assert response.status == 200
app.remove_route('/folder/<name>')
request, response = sanic_endpoint_test(app, uri='/folder/test123')
assert response.status == 404
def test_remove_inexistent_route():
app = Sanic('test_remove_inexistent_route')
with pytest.raises(RouteDoesNotExist):
app.remove_route('/test')
def test_remove_unhashable_route():
app = Sanic('test_remove_unhashable_route')
async def handler(request, unhashable):
return text('OK')
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/')
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
assert response.status == 200
app.remove_route('/folder/<unhashable:[A-Za-z0-9/]+>/end/')
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
assert response.status == 404
def test_remove_route_without_clean_cache():
app = Sanic('test_remove_static_route')
async def handler(request):
return text('OK')
app.add_route(handler, '/test')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
app.remove_route('/test', clean_cache=True)
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 404
app.add_route(handler, '/test')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
app.remove_route('/test', clean_cache=False)
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
def test_overload_routes():
app = Sanic('test_dynamic_route')
@app.route('/overload', methods=['GET'])
async def handler1(request):
return text('OK1')
@app.route('/overload', methods=['POST', 'PUT'])
async def handler2(request):
return text('OK2')
request, response = sanic_endpoint_test(app, 'get', uri='/overload')
assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, 'post', uri='/overload')
assert response.text == 'OK2'
request, response = sanic_endpoint_test(app, 'put', uri='/overload')
assert response.text == 'OK2'
request, response = sanic_endpoint_test(app, 'delete', uri='/overload')
assert response.status == 405
with pytest.raises(RouteExists):
@app.route('/overload', methods=['PUT', 'DELETE'])
async def handler3(request):
return text('Duplicated')
def test_unmergeable_overload_routes():
app = Sanic('test_dynamic_route')
@app.route('/overload_whole', methods=None)
async def handler1(request):
return text('OK1')
with pytest.raises(RouteExists):
@app.route('/overload_whole', methods=['POST', 'PUT'])
async def handler2(request):
return text('Duplicated')
request, response = sanic_endpoint_test(app, 'get', uri='/overload_whole')
assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, 'post', uri='/overload_whole')
assert response.text == 'OK1'
@app.route('/overload_part', methods=['GET'])
async def handler1(request):
return text('OK1')
with pytest.raises(RouteExists):
@app.route('/overload_part')
async def handler2(request):
return text('Duplicated')
request, response = sanic_endpoint_test(app, 'get', uri='/overload_part')
assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, 'post', uri='/overload_part')
assert response.status == 405

View File

@ -0,0 +1,59 @@
from io import StringIO
from random import choice
from string import ascii_letters
import signal
import pytest
from sanic import Sanic
AVAILABLE_LISTENERS = [
'before_start',
'after_start',
'before_stop',
'after_stop'
]
def create_listener(listener_name, in_list):
async def _listener(app, loop):
print('DEBUG MESSAGE FOR PYTEST for {}'.format(listener_name))
in_list.insert(0, app.name + listener_name)
return _listener
def start_stop_app(random_name_app, **run_kwargs):
def stop_on_alarm(signum, frame):
raise KeyboardInterrupt('SIGINT for sanic to stop gracefully')
signal.signal(signal.SIGALRM, stop_on_alarm)
signal.alarm(1)
try:
random_name_app.run(**run_kwargs)
except KeyboardInterrupt:
pass
@pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS)
def test_single_listener(listener_name):
"""Test that listeners on their own work"""
random_name_app = Sanic(''.join(
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
output = list()
start_stop_app(
random_name_app,
**{listener_name: create_listener(listener_name, output)})
assert random_name_app.name + listener_name == output.pop()
def test_all_listeners():
random_name_app = Sanic(''.join(
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
output = list()
start_stop_app(
random_name_app,
**{listener_name: create_listener(listener_name, output)
for listener_name in AVAILABLE_LISTENERS})
for listener_name in AVAILABLE_LISTENERS:
assert random_name_app.name + listener_name == output.pop()

39
tests/test_vhosts.py Normal file
View File

@ -0,0 +1,39 @@
from sanic import Sanic
from sanic.response import json, text
from sanic.utils import sanic_endpoint_test
def test_vhosts():
app = Sanic('test_vhosts')
@app.route('/', host="example.com")
async def handler(request):
return text("You're at example.com!")
@app.route('/', host="subdomain.example.com")
async def handler(request):
return text("You're at subdomain.example.com!")
headers = {"Host": "example.com"}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == "You're at example.com!"
headers = {"Host": "subdomain.example.com"}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == "You're at subdomain.example.com!"
def test_vhosts_with_list():
app = Sanic('test_vhosts')
@app.route('/', host=["hello.com", "world.com"])
async def handler(request):
return text("Hello, world!")
headers = {"Host": "hello.com"}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == "Hello, world!"
headers = {"Host": "world.com"}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == "Hello, world!"

View File

@ -26,7 +26,7 @@ def test_methods():
def delete(self, request): def delete(self, request):
return text('I am delete method') return text('I am delete method')
app.add_route(DummyView(), '/') app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get") request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method' assert response.text == 'I am get method'
@ -48,7 +48,7 @@ def test_unexisting_methods():
def get(self, request): def get(self, request):
return text('I am get method') return text('I am get method')
app.add_route(DummyView(), '/') app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get") request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method' assert response.text == 'I am get method'
request, response = sanic_endpoint_test(app, method="post") request, response = sanic_endpoint_test(app, method="post")
@ -63,7 +63,7 @@ def test_argument_methods():
def get(self, request, my_param_here): def get(self, request, my_param_here):
return text('I am get method with %s' % my_param_here) return text('I am get method with %s' % my_param_here)
app.add_route(DummyView(), '/<my_param_here>') app.add_route(DummyView.as_view(), '/<my_param_here>')
request, response = sanic_endpoint_test(app, uri='/test123') request, response = sanic_endpoint_test(app, uri='/test123')
@ -79,7 +79,7 @@ def test_with_bp():
def get(self, request): def get(self, request):
return text('I am get method') return text('I am get method')
bp.add_route(DummyView(), '/') bp.add_route(DummyView.as_view(), '/')
app.blueprint(bp) app.blueprint(bp)
request, response = sanic_endpoint_test(app) request, response = sanic_endpoint_test(app)
@ -96,7 +96,7 @@ def test_with_bp_with_url_prefix():
def get(self, request): def get(self, request):
return text('I am get method') return text('I am get method')
bp.add_route(DummyView(), '/') bp.add_route(DummyView.as_view(), '/')
app.blueprint(bp) app.blueprint(bp)
request, response = sanic_endpoint_test(app, uri='/test1/') request, response = sanic_endpoint_test(app, uri='/test1/')
@ -112,7 +112,7 @@ def test_with_middleware():
def get(self, request): def get(self, request):
return text('I am get method') return text('I am get method')
app.add_route(DummyView(), '/') app.add_route(DummyView.as_view(), '/')
results = [] results = []
@ -145,7 +145,7 @@ def test_with_middleware_response():
def get(self, request): def get(self, request):
return text('I am get method') return text('I am get method')
app.add_route(DummyView(), '/') app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app) request, response = sanic_endpoint_test(app)
@ -153,3 +153,44 @@ def test_with_middleware_response():
assert type(results[0]) is Request assert type(results[0]) is Request
assert type(results[1]) is Request assert type(results[1]) is Request
assert issubclass(type(results[2]), HTTPResponse) assert issubclass(type(results[2]), HTTPResponse)
def test_with_custom_class_methods():
app = Sanic('test_with_custom_class_methods')
class DummyView(HTTPMethodView):
global_var = 0
def _iternal_method(self):
self.global_var += 10
def get(self, request):
self._iternal_method()
return text('I am get method and global var is {}'.format(self.global_var))
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method and global var is 10'
def test_with_decorator():
app = Sanic('test_with_decorator')
results = []
def stupid_decorator(view):
def decorator(*args, **kwargs):
results.append(1)
return view(*args, **kwargs)
return decorator
class DummyView(HTTPMethodView):
decorators = [stupid_decorator]
def get(self, request):
return text('I am get method')
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method'
assert results[0] == 1

View File

@ -13,6 +13,7 @@ python =
deps = deps =
aiohttp aiohttp
pytest pytest
beautifulsoup4
commands = commands =
pytest tests {posargs} pytest tests {posargs}