Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
419156f7cc | ||
|
|
b7f7883fb7 | ||
|
|
a5a7490bca | ||
|
|
6724d8131c | ||
|
|
a19bb15070 | ||
|
|
2ee0147848 | ||
|
|
252f1add7e | ||
|
|
413c92c631 | ||
|
|
36d519026f | ||
|
|
aa54785918 | ||
|
|
d6b386083f | ||
|
|
82f383b64f | ||
|
|
a15ee3ad06 | ||
|
|
29680cb515 | ||
|
|
884749f345 | ||
|
|
5c63ce666c | ||
|
|
5632d073be | ||
|
|
f9056099f9 | ||
|
|
7c09ec29f7 | ||
|
|
6f0b09509e | ||
|
|
31c53d67e2 | ||
|
|
6a322ba3f8 | ||
|
|
dece636d54 | ||
|
|
b29f648148 | ||
|
|
c91d264ff1 | ||
|
|
487e3352e4 | ||
|
|
34966fb182 | ||
|
|
17a92a58b2 | ||
|
|
60d3e5b9e0 | ||
|
|
1501c56bbc | ||
|
|
6d18fb6bae | ||
|
|
eac26a4514 | ||
|
|
1649f30808 | ||
|
|
82680bf43f | ||
|
|
41da793b5a | ||
|
|
cfb5734b85 | ||
|
|
b72d841619 | ||
|
|
0ef39f35ae | ||
|
|
38d1ed76d2 | ||
|
|
4c80cd185f | ||
|
|
629524af04 | ||
|
|
a245e54bd3 | ||
|
|
52e485cce9 | ||
|
|
02d374b65b | ||
|
|
0de6bb0063 | ||
|
|
9d6b379999 | ||
|
|
c132c4e673 | ||
|
|
6962dcd66c | ||
|
|
2a9496fcda | ||
|
|
82d1d30a41 | ||
|
|
85639d0806 | ||
|
|
3fd6ecaedb | ||
|
|
884d3a0316 | ||
|
|
10dbb9186d | ||
|
|
a547798b08 | ||
|
|
894b434875 | ||
|
|
8db0ece459 | ||
|
|
f56c5e3a45 | ||
|
|
0a5fa72099 | ||
|
|
0eaccea38f | ||
|
|
de32c389d0 | ||
|
|
753d2da6db | ||
|
|
d3344da9c5 | ||
|
|
ae0876876e | ||
|
|
dea8e16f49 | ||
|
|
13803bdb30 | ||
|
|
7257e5794f | ||
|
|
41c52487ee | ||
|
|
0eb779185d | ||
|
|
5bba3388a0 | ||
|
|
0b9094d348 | ||
|
|
2d4512cd1c | ||
|
|
a73a7d1e7b | ||
|
|
ef9edfd160 | ||
|
|
234a7925c6 | ||
|
|
a550b5c112 | ||
|
|
04798cbf5b |
@@ -1,14 +1,14 @@
|
||||
sudo: false
|
||||
language: python
|
||||
python:
|
||||
- '3.5'
|
||||
- '3.6'
|
||||
- '3.5'
|
||||
- '3.6'
|
||||
install: pip install tox-travis
|
||||
script: tox
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: channelcat
|
||||
password:
|
||||
secure: jH4+Di2/qcBwWVhI5/3NYd/JuDDgf5/cF85h+oQnAjgwP6me3th9RS0PHL2gjKJrmyRgwrW7a3eSAityo5sQSlBloQCNrtCE30rkDiwtgoIxDW72NR/nE8nUkS9Utgy87eS+3B4NrO7ag4GTqO5ET8SQ4/MCiQwyUQATLXj2s2eTpQvqJeZG6YgoeFAOYvlR580yznXoOwldWlkiymJiWSdR/01lthtWCi40sYC/QoU7psODJ/tPcsqgQtQKyUVsci7mKvp3Y8ImkoO/POM01jYNsS9qLh5pKTNCEYxtyzC77whenCNHn7WReVidd56g1ADosbNo4yY/1D3VAvwjUnkQ0SzdBQfT7IIzccEuC0j1NXKPN97OX0a6XzyUMYJ1XiU3juTJOPxdYBPbsDM3imQiwrOh1faIf0HCgNTN+Lxe5l8obCH7kffNcVUhs2zI0+2t4MS5tjb/OVuYD/TFn+bM33DqzLctTOK/pGn6xefzZcdzb191LPo99Lof+4fo6jNUpb0UmcBu5ZJzxh0lGe8FPIK3UAG/hrYDDgjx8s8RtUJjcEUQz0659XffYx7DLlgHO7cWyfjrHD3yrLzDbYr5mAS4FR+4D917V7UL+on4SsKHN00UuMGPguqSYo/xYyPLnJU5XK0du4MIpsNMB8TtrJOIewOOfD32+AisPQ8=
|
||||
secure: OgADRQH3+dTL5swGzXkeRJDNbLpFzwqYnXB4iLD0Npvzj9QnKyQVvkbaeq6VmV9dpEFb5ULaAKYQq19CrXYDm28yanUSn6jdJ4SukaHusi7xt07U6H7pmoX/uZ2WZYqCSLM8cSp8TXY/3oV3rY5Jfj/AibE5XTbim5/lrhsvW6NR+ALzxc0URRPAHDZEPpojTCjSTjpY0aDsaKWg4mXVRMFfY3O68j6KaIoukIZLuoHfePLKrbZxaPG5VxNhMHEaICdxVxE/dO+7pQmQxXuIsEOHK1QiVJ9YrSGcNqgEqhN36kYP8dqMeVB07sv8Xa6o/Uax2/wXS2HEJvuwP1YD6WkoZuo9ZB85bcMdg7BV9jJDbVFVPJwc75BnTLHrMa3Q1KrRlKRDBUXBUsQivPuWhFNwUgvEayq2qSI3aRQR4Z0O+DfboEhXYojSoD64/EWBTZ7vhgbvOTGEdukUQSYrKj9P8jc1s8exomTsAiqdFxTUpzfiammUSL+M93lP4urtahl1jjXFX7gd3DzdEEb0NsGkx5lm/qdsty8/TeAvKUmC+RVU6T856W6MqN0P+yGbpWUARcSE7fwztC3SPxwAuxvIN3BHmRhOUHoORPNG2VpfbnscIzBKJR4v0JKzbpi0IDa66K+tCGsCEvQuL4cxVOtoUySPWNSUAyUWWUrGM2k=
|
||||
on:
|
||||
tags: true
|
||||
|
||||
21
README.rst
21
README.rst
@@ -45,10 +45,8 @@ Hello World Example
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
@@ -56,21 +54,6 @@ Hello World Example
|
||||
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
|
||||
------------
|
||||
|
||||
@@ -79,12 +62,14 @@ Installation
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Documentation can be found in the ``docs`` directory.
|
||||
`Documentation on Readthedocs <http://sanic.readthedocs.io/>`_.
|
||||
|
||||
.. |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
|
||||
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
|
||||
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
|
||||
.. |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
|
||||
|
||||
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = Sanic
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
10
docs/conf.py
10
docs/conf.py
@@ -22,9 +22,7 @@ import sanic
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.githubpages']
|
||||
extensions = []
|
||||
|
||||
templates_path = ['_templates']
|
||||
|
||||
@@ -75,7 +73,7 @@ todo_include_todos = False
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'alabaster'
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# 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,
|
||||
@@ -153,3 +151,7 @@ epub_copyright = copyright
|
||||
epub_exclude_files = ['search.html']
|
||||
|
||||
|
||||
|
||||
# -- Custom Settings -------------------------------------------------------
|
||||
|
||||
suppress_warnings = ['image.nonlocal_uri']
|
||||
|
||||
78
docs/config.md
Normal file
78
docs/config.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Configuration
|
||||
|
||||
Any reasonably complex application will need configuration that is not baked into the acutal code. Settings might be different for different environments or installations.
|
||||
|
||||
## Basics
|
||||
|
||||
Sanic holds the configuration in the `config` attribute of the application object. The configuration object is merely an object that can be modified either using dot-notation or like a dictionary:
|
||||
|
||||
```
|
||||
app = Sanic('myapp')
|
||||
app.config.DB_NAME = 'appdb'
|
||||
app.config.DB_USER = 'appuser'
|
||||
```
|
||||
|
||||
Since the config object actually is a dictionary, you can use its `update` method in order to set several values at once:
|
||||
|
||||
```
|
||||
db_settings = {
|
||||
'DB_HOST': 'localhost',
|
||||
'DB_NAME': 'appdb',
|
||||
'DB_USER': 'appuser'
|
||||
}
|
||||
app.config.update(db_settings)
|
||||
```
|
||||
|
||||
In general the convention is to only have UPPERCASE configuration parameters. The methods described below for loading configuration only look for such uppercase parameters.
|
||||
|
||||
## Loading Configuration
|
||||
|
||||
There are several ways how to load configuration.
|
||||
|
||||
### From an Object
|
||||
|
||||
If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module:
|
||||
|
||||
```
|
||||
import myapp.default_settings
|
||||
|
||||
app = Sanic('myapp')
|
||||
app.config.from_object(myapp.default_settings)
|
||||
```
|
||||
|
||||
You could use a class or any other object as well.
|
||||
|
||||
### From a File
|
||||
|
||||
Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_file(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file:
|
||||
|
||||
```
|
||||
app = Sanic('myapp')
|
||||
app.config.from_envvar('MYAPP_SETTINGS')
|
||||
```
|
||||
|
||||
Then you can run your application with the `MYAPP_SETTINGS` environment variable set:
|
||||
|
||||
```
|
||||
$ MYAPP_SETTINGS=/path/to/config_file python3 myapp.py
|
||||
INFO: Goin' Fast @ http://0.0.0.0:8000
|
||||
```
|
||||
|
||||
The config files are regular Python files which are executed in order to load them. This allows you to use arbitrary logic for constructing the right configuration. Only uppercase varibales are added to the configuration. Most commonly the configuration consists of simple key value pairs:
|
||||
|
||||
```
|
||||
# config_file
|
||||
DB_HOST = 'localhost'
|
||||
DB_NAME = 'appdb'
|
||||
DB_USER = 'appuser'
|
||||
```
|
||||
|
||||
## Builtin Configuration Values
|
||||
|
||||
Out of the box there are just a few predefined values which can be overwritten when creating the application.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ----------------- | --------- | --------------------------------- |
|
||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.. include:: ../README.rst
|
||||
.. include:: sanic/index.rst
|
||||
|
||||
Guides
|
||||
======
|
||||
@@ -6,20 +6,22 @@ Guides
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
getting_started
|
||||
routing
|
||||
request_data
|
||||
deploying
|
||||
static_files
|
||||
middleware
|
||||
exceptions
|
||||
blueprints
|
||||
class_based_views
|
||||
cookies
|
||||
custom_protocol
|
||||
testing
|
||||
extensions
|
||||
contributing
|
||||
sanic/getting_started
|
||||
sanic/routing
|
||||
sanic/request_data
|
||||
sanic/static_files
|
||||
sanic/exceptions
|
||||
sanic/middleware
|
||||
sanic/blueprints
|
||||
sanic/config
|
||||
sanic/cookies
|
||||
sanic/class_based_views
|
||||
sanic/custom_protocol
|
||||
sanic/ssl
|
||||
sanic/testing
|
||||
sanic/deploying
|
||||
sanic/extensions
|
||||
sanic/contributing
|
||||
|
||||
|
||||
Module Documentation
|
||||
@@ -27,7 +29,5 @@ Module Documentation
|
||||
|
||||
.. toctree::
|
||||
|
||||
Module Reference <_api/sanic>
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`search`
|
||||
|
||||
36
docs/make.bat
Normal file
36
docs/make.bat
Normal file
@@ -0,0 +1,36 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
set SPHINXPROJ=Sanic
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
|
||||
:end
|
||||
popd
|
||||
@@ -159,6 +159,21 @@ app.blueprint(blueprint_v2)
|
||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||
```
|
||||
|
||||
**Previous:** [Exceptions](exceptions.md)
|
||||
## URL Building with `url_for`
|
||||
|
||||
If you wish to generate a URL for a route inside of a blueprint, remember that the endpoint name
|
||||
takes the format `<blueprint_name>.<handler_name>`. For example:
|
||||
|
||||
```
|
||||
@blueprint_v1.route('/')
|
||||
async def root(request):
|
||||
url = app.url_for('v1.post_handler', post_id=5)
|
||||
return redirect(url)
|
||||
|
||||
|
||||
@blueprint_v1.route('/post/<post_id>')
|
||||
async def post_handler(request, post_id):
|
||||
return text('Post {} in Blueprint V1'.format(post_id))
|
||||
```
|
||||
|
||||
|
||||
**Next:** [Class-based views](class_based_views.md)
|
||||
@@ -67,7 +67,7 @@ app.add_route(NameView.as_view(), '/<name>')
|
||||
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.
|
||||
|
||||
```
|
||||
```python
|
||||
class ViewWithDecorator(HTTPMethodView):
|
||||
decorators = [some_decorator_here]
|
||||
|
||||
@@ -77,6 +77,27 @@ class ViewWithDecorator(HTTPMethodView):
|
||||
app.add_route(ViewWithDecorator.as_view(), '/url')
|
||||
```
|
||||
|
||||
#### URL Building
|
||||
|
||||
If you wish to build a URL for an HTTPMethodView, remember that the class name will be the endpoint
|
||||
that you will pass into `url_for`. For example:
|
||||
|
||||
```python
|
||||
@app.route('/')
|
||||
def index(request):
|
||||
url = app.url_for('SpecialClassView')
|
||||
return redirect(url)
|
||||
|
||||
|
||||
class SpecialClassView(HTTPMethodView):
|
||||
def get(self, request):
|
||||
return text('Hello from the Special Class View!')
|
||||
|
||||
|
||||
app.add_route(SpecialClassView.as_view(), '/special_class_view')
|
||||
```
|
||||
|
||||
|
||||
## Using CompositionView
|
||||
|
||||
As an alternative to the `HTTPMethodView`, you can use `CompositionView` to
|
||||
@@ -107,6 +128,4 @@ view.add(['POST', 'PUT'], lambda request: text('I am a post/put method'))
|
||||
app.add_route(view, '/')
|
||||
```
|
||||
|
||||
**Previous:** [Blueprints](blueprints.md)
|
||||
|
||||
**Next:** [Cookies](cookies.md)
|
||||
Note: currently you cannot build a URL for a CompositionView using `url_for`.
|
||||
@@ -31,5 +31,3 @@ 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)
|
||||
@@ -73,7 +73,3 @@ parameters available:
|
||||
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)
|
||||
@@ -70,7 +70,3 @@ async def response(request):
|
||||
|
||||
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)
|
||||
```
|
||||
|
||||
**Previous:** [Cookies](cookies.md)
|
||||
|
||||
**Next:** [Testing](testing.md)
|
||||
@@ -53,7 +53,3 @@ directly run by the interpreter.
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
||||
```
|
||||
|
||||
**Previous:** [Request Data](request_data.md)
|
||||
|
||||
**Next:** [Static Files](static_files.md)
|
||||
@@ -43,7 +43,3 @@ Some of the most useful exceptions are presented below:
|
||||
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)
|
||||
@@ -1,4 +1,4 @@
|
||||
# Sanic Extensions
|
||||
# Extensions
|
||||
|
||||
A list of Sanic extensions created by the community.
|
||||
|
||||
@@ -6,7 +6,4 @@ A list of Sanic extensions created by the community.
|
||||
Allows using redis, memcache or an in memory store.
|
||||
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
|
||||
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
|
||||
|
||||
**Previous:** [Testing](testing.md)
|
||||
|
||||
**Next:** [Contributing](contributing.md)
|
||||
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
|
||||
@@ -25,5 +25,3 @@ syntax, so earlier versions of python won't work.
|
||||
the message *Hello world!*.
|
||||
|
||||
You now have a working Sanic server!
|
||||
|
||||
**Next:** [Routing](routing.md)
|
||||
25
docs/sanic/index.rst
Normal file
25
docs/sanic/index.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
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.
|
||||
|
||||
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
|
||||
|
||||
Sanic aspires to be simple:
|
||||
-------------------
|
||||
|
||||
.. 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)
|
||||
@@ -64,7 +64,3 @@ async def halt_request(request):
|
||||
async def halt_response(request, response):
|
||||
return text('I halted the response')
|
||||
```
|
||||
|
||||
**Previous:** [Static Files](static_files.md)
|
||||
|
||||
**Next:** [Exceptions](exceptions.md)
|
||||
@@ -89,7 +89,3 @@ args.get('titles') # => 'Post 1'
|
||||
|
||||
args.getlist('titles') # => ['Post 1', 'Post 2']
|
||||
```
|
||||
|
||||
**Previous:** [Routing](routing.md)
|
||||
|
||||
**Next:** [Deploying](deploying.md)
|
||||
@@ -64,23 +64,37 @@ async def folder_handler(request, folder_id):
|
||||
|
||||
## HTTP request types
|
||||
|
||||
By default, a route defined on a URL will be used for all requests to that URL.
|
||||
By default, a route defined on a URL will be avaialble for only GET requests to that URL.
|
||||
However, the `@app.route` decorator accepts an optional parameter, `methods`,
|
||||
which restricts the handler function to the HTTP methods in the given list.
|
||||
whicl allows the handler function to work with any of the HTTP methods in the list.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/post')
|
||||
async def post_handler(request, methods=['POST']):
|
||||
@app.route('/post', methods=['POST'])
|
||||
async def post_handler(request):
|
||||
return text('POST request - {}'.format(request.json))
|
||||
|
||||
@app.route('/get')
|
||||
async def GET_handler(request, methods=['GET']):
|
||||
@app.route('/get', methods=['GET'])
|
||||
async def get_handler(request):
|
||||
return text('GET request - {}'.format(request.args))
|
||||
|
||||
```
|
||||
|
||||
There are also shorthand method decorators:
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.post('/post')
|
||||
async def post_handler(request):
|
||||
return text('POST request - {}'.format(request.json))
|
||||
|
||||
@app.get('/get')
|
||||
async def get_handler(request):
|
||||
return text('GET request - {}'.format(request.args))
|
||||
|
||||
```
|
||||
## The `add_route` method
|
||||
|
||||
As we have seen, routes are often specified using the `@app.route` decorator.
|
||||
@@ -106,6 +120,34 @@ app.add_route(handler2, '/folder/<name>')
|
||||
app.add_route(person_handler2, '/person/<name:[A-z]>', methods=['GET'])
|
||||
```
|
||||
|
||||
**Previous:** [Getting Started](getting_started.md)
|
||||
## URL building with `url_for`
|
||||
|
||||
Sanic provides a `url_for` method, to generate URLs based on the handler method name. This is useful if you want to avoid hardcoding url paths into your app; instead, you can just reference the handler name. For example:
|
||||
|
||||
```python
|
||||
@app.route('/')
|
||||
async def index(request):
|
||||
# generate a URL for the endpoint `post_handler`
|
||||
url = app.url_for('post_handler', post_id=5)
|
||||
# the URL is `/posts/5`, redirect to it
|
||||
return redirect(url)
|
||||
|
||||
|
||||
@app.route('/posts/<post_id>')
|
||||
async def post_handler(request, post_id):
|
||||
return text('Post - {}'.format(post_id))
|
||||
```
|
||||
|
||||
Other things to keep in mind when using `url_for`:
|
||||
|
||||
- Keyword arguments passed to `url_for` that are not request parameters will be included in the URL's query string. For example:
|
||||
```python
|
||||
url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two')
|
||||
# /posts/5?arg_one=one&arg_two=two
|
||||
```
|
||||
- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be thrown.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
**Next:** [Request Data](request_data.md)
|
||||
12
docs/sanic/ssl.rst
Normal file
12
docs/sanic/ssl.rst
Normal file
@@ -0,0 +1,12 @@
|
||||
SSL Example
|
||||
-----------
|
||||
|
||||
Optionally pass in an SSLContext:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import ssl
|
||||
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain("/path/to/cert", keyfile="/path/to/keyfile")
|
||||
|
||||
app.run(host="0.0.0.0", port=8443, ssl=context)
|
||||
@@ -18,6 +18,4 @@ app.static('/the_best.png', '/home/ubuntu/test.png')
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
**Previous:** [Deploying](deploying.md)
|
||||
|
||||
**Next:** [Middleware](middleware.md)
|
||||
Note: currently you cannot build a URL for a static file using `url_for`.
|
||||
@@ -49,7 +49,3 @@ def test_endpoint_challenge():
|
||||
# Assert that the server responds with the challenge string
|
||||
assert response.text == request_data['challenge']
|
||||
```
|
||||
|
||||
**Previous:** [Custom protocols](custom_protocol.md)
|
||||
|
||||
**Next:** [Sanic extensions](extensions.md)
|
||||
18
environment.yml
Normal file
18
environment.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
name: py35
|
||||
dependencies:
|
||||
- openssl=1.0.2g=0
|
||||
- pip=8.1.1=py35_0
|
||||
- python=3.5.1=0
|
||||
- readline=6.2=2
|
||||
- setuptools=20.3=py35_0
|
||||
- sqlite=3.9.2=0
|
||||
- tk=8.5.18=0
|
||||
- wheel=0.29.0=py35_0
|
||||
- xz=5.0.5=1
|
||||
- zlib=1.2.8=0
|
||||
- pip:
|
||||
- uvloop>=0.5.3
|
||||
- httptools>=0.0.9
|
||||
- ujson>=1.35
|
||||
- aiofiles>=0.3.0
|
||||
- https://github.com/channelcat/docutils-fork/zipball/master
|
||||
@@ -3,6 +3,7 @@ from sanic.response import json
|
||||
from multiprocessing import Event
|
||||
from signal import signal, SIGINT
|
||||
import asyncio
|
||||
import uvloop
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
@@ -10,10 +11,11 @@ app = Sanic(__name__)
|
||||
async def test(request):
|
||||
return json({"answer": "42"})
|
||||
|
||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||
server = app.create_server(host="0.0.0.0", port=8001)
|
||||
loop = asyncio.get_event_loop()
|
||||
task = asyncio.ensure_future(server)
|
||||
signal(SIGINT, lambda s, f: loop.close())
|
||||
signal(SIGINT, lambda s, f: loop.stop())
|
||||
try:
|
||||
loop.run_forever()
|
||||
except:
|
||||
|
||||
2
readthedocs.yml
Normal file
2
readthedocs.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
conda:
|
||||
file: environment.yml
|
||||
@@ -1,17 +1,18 @@
|
||||
aiocache
|
||||
aiofiles
|
||||
aiohttp
|
||||
beautifulsoup4
|
||||
bottle
|
||||
coverage
|
||||
falcon
|
||||
gunicorn
|
||||
httptools
|
||||
kyoukai
|
||||
pytest
|
||||
recommonmark
|
||||
sphinx
|
||||
sphinx_rtd_theme
|
||||
tornado
|
||||
tox
|
||||
ujson
|
||||
uvloop
|
||||
aiohttp
|
||||
aiocache
|
||||
pytest
|
||||
coverage
|
||||
tox
|
||||
gunicorn
|
||||
bottle
|
||||
kyoukai
|
||||
falcon
|
||||
tornado
|
||||
aiofiles
|
||||
sphinx
|
||||
recommonmark
|
||||
beautifulsoup4
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
aiofiles
|
||||
httptools
|
||||
ujson
|
||||
uvloop
|
||||
aiofiles
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from .sanic import Sanic
|
||||
from .blueprints import Blueprint
|
||||
|
||||
__version__ = '0.3.0'
|
||||
__version__ = '0.3.1'
|
||||
|
||||
__all__ = ['Sanic', 'Blueprint']
|
||||
|
||||
@@ -1,59 +1,12 @@
|
||||
from collections import defaultdict
|
||||
from collections import defaultdict, namedtuple
|
||||
|
||||
|
||||
class BlueprintSetup:
|
||||
"""
|
||||
Creates a blueprint state like object.
|
||||
"""
|
||||
|
||||
def __init__(self, blueprint, app, options):
|
||||
self.app = app
|
||||
self.blueprint = blueprint
|
||||
self.options = options
|
||||
|
||||
url_prefix = self.options.get('url_prefix')
|
||||
if url_prefix is None:
|
||||
url_prefix = self.blueprint.url_prefix
|
||||
|
||||
#: The prefix that should be used for all URLs defined on the
|
||||
#: blueprint.
|
||||
self.url_prefix = url_prefix
|
||||
|
||||
def add_route(self, handler, uri, methods, host=None):
|
||||
"""
|
||||
A helper method to register a handler to the application url routes.
|
||||
"""
|
||||
if self.url_prefix:
|
||||
uri = self.url_prefix + uri
|
||||
|
||||
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):
|
||||
"""
|
||||
Registers exceptions to sanic.
|
||||
"""
|
||||
self.app.exception(*args, **kwargs)(handler)
|
||||
|
||||
def add_static(self, uri, file_or_directory, *args, **kwargs):
|
||||
"""
|
||||
Registers static files to sanic.
|
||||
"""
|
||||
if self.url_prefix:
|
||||
uri = self.url_prefix + uri
|
||||
|
||||
self.app.static(uri, file_or_directory, *args, **kwargs)
|
||||
|
||||
def add_middleware(self, middleware, *args, **kwargs):
|
||||
"""
|
||||
Registers middleware to sanic.
|
||||
"""
|
||||
if args or kwargs:
|
||||
self.app.middleware(*args, **kwargs)(middleware)
|
||||
else:
|
||||
self.app.middleware(middleware)
|
||||
FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host'])
|
||||
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
|
||||
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
|
||||
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
|
||||
FutureStatic = namedtuple('Route',
|
||||
['uri', 'file_or_directory', 'args', 'kwargs'])
|
||||
|
||||
|
||||
class Blueprint:
|
||||
@@ -65,30 +18,52 @@ class Blueprint:
|
||||
"""
|
||||
self.name = name
|
||||
self.url_prefix = url_prefix
|
||||
self.deferred_functions = []
|
||||
self.listeners = defaultdict(list)
|
||||
self.host = host
|
||||
|
||||
def record(self, func):
|
||||
"""
|
||||
Registers a callback function that is invoked when the blueprint is
|
||||
registered on the application.
|
||||
"""
|
||||
self.deferred_functions.append(func)
|
||||
|
||||
def make_setup_state(self, app, options):
|
||||
"""
|
||||
Returns a new BlueprintSetup object
|
||||
"""
|
||||
return BlueprintSetup(self, app, options)
|
||||
self.routes = []
|
||||
self.exceptions = []
|
||||
self.listeners = defaultdict(list)
|
||||
self.middlewares = []
|
||||
self.statics = []
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Registers the blueprint to the sanic app.
|
||||
"""
|
||||
state = self.make_setup_state(app, options)
|
||||
for deferred in self.deferred_functions:
|
||||
deferred(state)
|
||||
|
||||
url_prefix = options.get('url_prefix', self.url_prefix)
|
||||
|
||||
# Routes
|
||||
for future in self.routes:
|
||||
# attach the blueprint name to the handler so that it can be
|
||||
# prefixed properly in the router
|
||||
future.handler.__blueprintname__ = self.name
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
app.route(
|
||||
uri=uri,
|
||||
methods=future.methods,
|
||||
host=future.host or self.host
|
||||
)(future.handler)
|
||||
|
||||
# Middleware
|
||||
for future in self.middlewares:
|
||||
if future.args or future.kwargs:
|
||||
app.middleware(*future.args,
|
||||
**future.kwargs)(future.middleware)
|
||||
else:
|
||||
app.middleware(future.middleware)
|
||||
|
||||
# Exceptions
|
||||
for future in self.exceptions:
|
||||
app.exception(*future.args, **future.kwargs)(future.handler)
|
||||
|
||||
# Static Files
|
||||
for future in self.statics:
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
app.static(uri, future.file_or_directory,
|
||||
*future.args, **future.kwargs)
|
||||
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None):
|
||||
"""
|
||||
@@ -97,7 +72,8 @@ class Blueprint:
|
||||
:param methods: List of acceptable HTTP methods.
|
||||
"""
|
||||
def decorator(handler):
|
||||
self.record(lambda s: s.add_route(handler, uri, methods, host))
|
||||
route = FutureRoute(handler, uri, methods, host)
|
||||
self.routes.append(route)
|
||||
return handler
|
||||
return decorator
|
||||
|
||||
@@ -108,7 +84,8 @@ class Blueprint:
|
||||
: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, host))
|
||||
route = FutureRoute(handler, uri, methods, host)
|
||||
self.routes.append(route)
|
||||
return handler
|
||||
|
||||
def listener(self, event):
|
||||
@@ -125,10 +102,10 @@ class Blueprint:
|
||||
"""
|
||||
Creates a blueprint middleware from a decorated function.
|
||||
"""
|
||||
def register_middleware(middleware):
|
||||
self.record(
|
||||
lambda s: s.add_middleware(middleware, *args, **kwargs))
|
||||
return middleware
|
||||
def register_middleware(_middleware):
|
||||
future_middleware = FutureMiddleware(_middleware, args, kwargs)
|
||||
self.middlewares.append(future_middleware)
|
||||
return _middleware
|
||||
|
||||
# Detect which way this was called, @middleware or @middleware('AT')
|
||||
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
|
||||
@@ -143,7 +120,8 @@ class Blueprint:
|
||||
Creates a blueprint exception from a decorated function.
|
||||
"""
|
||||
def decorator(handler):
|
||||
self.record(lambda s: s.add_exception(handler, *args, **kwargs))
|
||||
exception = FutureException(handler, args, kwargs)
|
||||
self.exceptions.append(exception)
|
||||
return handler
|
||||
return decorator
|
||||
|
||||
@@ -153,5 +131,27 @@ class Blueprint:
|
||||
:param uri: Endpoint at which the route will be accessible.
|
||||
:param file_or_directory: Static asset.
|
||||
"""
|
||||
self.record(
|
||||
lambda s: s.add_static(uri, file_or_directory, *args, **kwargs))
|
||||
static = FutureStatic(uri, file_or_directory, args, kwargs)
|
||||
self.statics.append(static)
|
||||
|
||||
# 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 delete(self, uri, host=None):
|
||||
return self.route(uri, methods=["DELETE"], host=host)
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
class Config:
|
||||
LOGO = """
|
||||
import os
|
||||
import types
|
||||
|
||||
|
||||
class Config(dict):
|
||||
def __init__(self, defaults=None):
|
||||
super().__init__(defaults or {})
|
||||
self.LOGO = """
|
||||
▄▄▄▄▄
|
||||
▀▀▀██████▄▄▄ _______________
|
||||
▄▄▄▄▄ █████████▄ / \\
|
||||
@@ -20,6 +26,65 @@ class Config:
|
||||
▌ ▐ ▀▀▄▄▄▀
|
||||
▀▀▄▄▀
|
||||
"""
|
||||
REQUEST_MAX_SIZE = 100000000 # 100 megababies
|
||||
REQUEST_TIMEOUT = 60 # 60 seconds
|
||||
ROUTER_CACHE_SIZE = 1024
|
||||
self.REQUEST_MAX_SIZE = 100000000 # 100 megababies
|
||||
self.REQUEST_TIMEOUT = 60 # 60 seconds
|
||||
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
return self[attr]
|
||||
except KeyError as ke:
|
||||
raise AttributeError("Config has no '{}'".format(ke.args[0]))
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
self[attr] = value
|
||||
|
||||
def from_envvar(self, variable_name):
|
||||
"""Loads a configuration from an environment variable pointing to
|
||||
a configuration file.
|
||||
:param variable_name: name of the environment variable
|
||||
:return: bool. ``True`` if able to load config, ``False`` otherwise.
|
||||
"""
|
||||
config_file = os.environ.get(variable_name)
|
||||
if not config_file:
|
||||
raise RuntimeError('The environment variable %r is not set and '
|
||||
'thus configuration could not be loaded.' %
|
||||
variable_name)
|
||||
return self.from_pyfile(config_file)
|
||||
|
||||
def from_pyfile(self, filename):
|
||||
"""Updates the values in the config from a Python file. Only the uppercase
|
||||
variables in that module are stored in the config.
|
||||
:param filename: an absolute path to the config file
|
||||
"""
|
||||
module = types.ModuleType('config')
|
||||
module.__file__ = filename
|
||||
try:
|
||||
with open(filename) as config_file:
|
||||
exec(compile(config_file.read(), filename, 'exec'),
|
||||
module.__dict__)
|
||||
except IOError as e:
|
||||
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
|
||||
raise
|
||||
self.from_object(module)
|
||||
return True
|
||||
|
||||
def from_object(self, obj):
|
||||
"""Updates the values from the given object.
|
||||
Objects are usually either modules or classes.
|
||||
|
||||
Just the uppercase variables in that object are stored in the config.
|
||||
Example usage::
|
||||
|
||||
from yourapplication import default_config
|
||||
app.config.from_object(default_config)
|
||||
|
||||
You should not use this function to load the actual configuration but
|
||||
rather configuration defaults. The actual config should be loaded
|
||||
with :meth:`from_pyfile` and ideally from a location not within the
|
||||
package because the package might be installed system wide.
|
||||
|
||||
:param obj: an object holding the configuration
|
||||
"""
|
||||
for key in dir(obj):
|
||||
if key.isupper():
|
||||
self[key] = getattr(obj, key)
|
||||
|
||||
1
sanic/constants.py
Normal file
1
sanic/constants.py
Normal file
@@ -0,0 +1 @@
|
||||
HTTP_METHODS = ('GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'PATCH', 'DELETE')
|
||||
@@ -120,6 +120,10 @@ class ServerError(SanicException):
|
||||
status_code = 500
|
||||
|
||||
|
||||
class URLBuildError(SanicException):
|
||||
status_code = 500
|
||||
|
||||
|
||||
class FileNotFound(NotFound):
|
||||
status_code = 404
|
||||
|
||||
@@ -139,9 +143,12 @@ class PayloadTooLarge(SanicException):
|
||||
|
||||
class Handler:
|
||||
handlers = None
|
||||
cached_handlers = None
|
||||
_missing = object()
|
||||
|
||||
def __init__(self):
|
||||
self.handlers = {}
|
||||
self.handlers = []
|
||||
self.cached_handlers = {}
|
||||
self.debug = False
|
||||
|
||||
def _render_traceback_html(self, exception, request):
|
||||
@@ -160,7 +167,18 @@ class Handler:
|
||||
uri=request.url)
|
||||
|
||||
def add(self, exception, handler):
|
||||
self.handlers[exception] = handler
|
||||
self.handlers.append((exception, handler))
|
||||
|
||||
def lookup(self, exception):
|
||||
handler = self.cached_handlers.get(exception, self._missing)
|
||||
if handler is self._missing:
|
||||
for exception_class, handler in self.handlers:
|
||||
if isinstance(exception, exception_class):
|
||||
self.cached_handlers[type(exception)] = handler
|
||||
return handler
|
||||
self.cached_handlers[type(exception)] = None
|
||||
handler = None
|
||||
return handler
|
||||
|
||||
def response(self, request, exception):
|
||||
"""
|
||||
@@ -170,9 +188,12 @@ class Handler:
|
||||
:param exception: Exception to handle
|
||||
:return: Response object
|
||||
"""
|
||||
handler = self.handlers.get(type(exception), self.default)
|
||||
handler = self.lookup(exception)
|
||||
try:
|
||||
response = handler(request=request, exception=exception)
|
||||
response = handler and handler(
|
||||
request=request, exception=exception)
|
||||
if response is None:
|
||||
response = self.default(request=request, exception=exception)
|
||||
except:
|
||||
log.error(format_exc())
|
||||
if self.debug:
|
||||
|
||||
125
sanic/router.py
125
sanic/router.py
@@ -1,11 +1,12 @@
|
||||
import re
|
||||
from collections import defaultdict, namedtuple
|
||||
from functools import lru_cache
|
||||
from .config import Config
|
||||
from .exceptions import NotFound, InvalidUsage
|
||||
from .views import CompositionView
|
||||
|
||||
Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters'])
|
||||
Route = namedtuple(
|
||||
'Route',
|
||||
['handler', 'methods', 'pattern', 'parameters', 'name'])
|
||||
Parameter = namedtuple('Parameter', ['name', 'cast'])
|
||||
|
||||
REGEX_TYPES = {
|
||||
@@ -15,6 +16,8 @@ REGEX_TYPES = {
|
||||
'alpha': (str, r'[A-Za-z]+'),
|
||||
}
|
||||
|
||||
ROUTER_CACHE_SIZE = 1024
|
||||
|
||||
|
||||
def url_hash(url):
|
||||
return url.count('/')
|
||||
@@ -58,6 +61,7 @@ class Router:
|
||||
routes_static = None
|
||||
routes_dynamic = None
|
||||
routes_always_check = None
|
||||
parameter_pattern = re.compile(r'<(.+?)>')
|
||||
|
||||
def __init__(self):
|
||||
self.routes_all = {}
|
||||
@@ -66,6 +70,29 @@ class Router:
|
||||
self.routes_always_check = []
|
||||
self.hosts = None
|
||||
|
||||
def parse_parameter_string(self, parameter_string):
|
||||
"""
|
||||
Parse a parameter string into its constituent name, type, and pattern
|
||||
For example:
|
||||
`parse_parameter_string('<param_one:[A-z]')` ->
|
||||
('param_one', str, '[A-z]')
|
||||
|
||||
:param parameter_string: String to parse
|
||||
:return: tuple containing
|
||||
(parameter_name, parameter_type, parameter_pattern)
|
||||
"""
|
||||
# We could receive NAME or NAME:PATTERN
|
||||
name = parameter_string
|
||||
pattern = 'string'
|
||||
if ':' in parameter_string:
|
||||
name, pattern = parameter_string.split(':', 1)
|
||||
|
||||
default = (str, pattern)
|
||||
# Pull from pre-configured types
|
||||
_type, pattern = REGEX_TYPES.get(pattern, default)
|
||||
|
||||
return name, _type, pattern
|
||||
|
||||
def add(self, uri, methods, handler, host=None):
|
||||
"""
|
||||
Adds a handler to the route list
|
||||
@@ -103,16 +130,11 @@ class Router:
|
||||
properties = {"unhashable": None}
|
||||
|
||||
def add_parameter(match):
|
||||
# We could receive NAME or NAME:PATTERN
|
||||
name = match.group(1)
|
||||
pattern = 'string'
|
||||
if ':' in name:
|
||||
name, pattern = name.split(':', 1)
|
||||
name, _type, pattern = self.parse_parameter_string(name)
|
||||
|
||||
default = (str, pattern)
|
||||
# Pull from pre-configured types
|
||||
_type, pattern = REGEX_TYPES.get(pattern, default)
|
||||
parameter = Parameter(name=name, cast=_type)
|
||||
parameter = Parameter(
|
||||
name=name, cast=_type)
|
||||
parameters.append(parameter)
|
||||
|
||||
# Mark the whole route as unhashable if it has the hash key in it
|
||||
@@ -124,7 +146,7 @@ class Router:
|
||||
|
||||
return '({})'.format(pattern)
|
||||
|
||||
pattern_string = re.sub(r'<(.+?)>', add_parameter, uri)
|
||||
pattern_string = re.sub(self.parameter_pattern, add_parameter, uri)
|
||||
pattern = re.compile(r'^{}$'.format(pattern_string))
|
||||
|
||||
def merge_route(route, methods, handler):
|
||||
@@ -149,13 +171,36 @@ class Router:
|
||||
handler=view, methods=methods.union(route.methods))
|
||||
return route
|
||||
|
||||
route = self.routes_all.get(uri)
|
||||
if parameters:
|
||||
# TODO: This is too complex, we need to reduce the complexity
|
||||
if properties['unhashable']:
|
||||
routes_to_check = self.routes_always_check
|
||||
ndx, route = self.check_dynamic_route_exists(
|
||||
pattern, routes_to_check)
|
||||
else:
|
||||
routes_to_check = self.routes_dynamic[url_hash(uri)]
|
||||
ndx, route = self.check_dynamic_route_exists(
|
||||
pattern, routes_to_check)
|
||||
if ndx != -1:
|
||||
# Pop the ndx of the route, no dups of the same route
|
||||
routes_to_check.pop(ndx)
|
||||
else:
|
||||
route = self.routes_all.get(uri)
|
||||
|
||||
if route:
|
||||
route = merge_route(route, methods, handler)
|
||||
else:
|
||||
# prefix the handler name with the blueprint name
|
||||
# if available
|
||||
if hasattr(handler, '__blueprintname__'):
|
||||
handler_name = '{}.{}'.format(
|
||||
handler.__blueprintname__, handler.__name__)
|
||||
else:
|
||||
handler_name = getattr(handler, '__name__', None)
|
||||
|
||||
route = Route(
|
||||
handler=handler, methods=methods, pattern=pattern,
|
||||
parameters=parameters)
|
||||
parameters=parameters, name=handler_name)
|
||||
|
||||
self.routes_all[uri] = route
|
||||
if properties['unhashable']:
|
||||
@@ -165,6 +210,14 @@ class Router:
|
||||
else:
|
||||
self.routes_static[uri] = route
|
||||
|
||||
@staticmethod
|
||||
def check_dynamic_route_exists(pattern, routes_to_check):
|
||||
for ndx, route in enumerate(routes_to_check):
|
||||
if route.pattern == pattern:
|
||||
return ndx, route
|
||||
else:
|
||||
return -1, None
|
||||
|
||||
def remove(self, uri, clean_cache=True, host=None):
|
||||
if host is not None:
|
||||
uri = host + uri
|
||||
@@ -184,6 +237,23 @@ class Router:
|
||||
if clean_cache:
|
||||
self._get.cache_clear()
|
||||
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def find_route_by_view_name(self, view_name):
|
||||
"""
|
||||
Find a route in the router based on the specified view name.
|
||||
|
||||
:param view_name: string of view name to search by
|
||||
:return: tuple containing (uri, Route)
|
||||
"""
|
||||
if not view_name:
|
||||
return (None, None)
|
||||
|
||||
for uri, route in self.routes_all.items():
|
||||
if route.name == view_name:
|
||||
return uri, route
|
||||
|
||||
return (None, None)
|
||||
|
||||
def get(self, request):
|
||||
"""
|
||||
Gets a request handler based on the URL of the request, or raises an
|
||||
@@ -198,7 +268,7 @@ class Router:
|
||||
return self._get(request.url, request.method,
|
||||
request.headers.get("Host", ''))
|
||||
|
||||
@lru_cache(maxsize=Config.ROUTER_CACHE_SIZE)
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def _get(self, url, method, host):
|
||||
"""
|
||||
Gets a request handler based on the URL of the request, or raises an
|
||||
@@ -210,29 +280,40 @@ class Router:
|
||||
url = host + url
|
||||
# Check against known static routes
|
||||
route = self.routes_static.get(url)
|
||||
method_not_supported = InvalidUsage(
|
||||
'Method {} not allowed for URL {}'.format(
|
||||
method, url), status_code=405)
|
||||
if route:
|
||||
if route.methods and method not in route.methods:
|
||||
raise method_not_supported
|
||||
match = route.pattern.match(url)
|
||||
else:
|
||||
route_found = False
|
||||
# Move on to testing all regex routes
|
||||
for route in self.routes_dynamic[url_hash(url)]:
|
||||
match = route.pattern.match(url)
|
||||
if match:
|
||||
route_found |= match is not None
|
||||
# Do early method checking
|
||||
if match and method in route.methods:
|
||||
break
|
||||
else:
|
||||
# Lastly, check against all regex routes that cannot be hashed
|
||||
for route in self.routes_always_check:
|
||||
match = route.pattern.match(url)
|
||||
if match:
|
||||
route_found |= match is not None
|
||||
# Do early method checking
|
||||
if match and method in route.methods:
|
||||
break
|
||||
else:
|
||||
# Route was found but the methods didn't match
|
||||
if route_found:
|
||||
raise method_not_supported
|
||||
raise NotFound('Requested URL {} not found'.format(url))
|
||||
|
||||
if route.methods and method not in route.methods:
|
||||
raise InvalidUsage(
|
||||
'Method {} not allowed for URL {}'.format(
|
||||
method, url), status_code=405)
|
||||
|
||||
kwargs = {p.name: p.cast(value)
|
||||
for value, p
|
||||
in zip(match.groups(1), route.parameters)}
|
||||
return route.handler, [], kwargs
|
||||
route_handler = route.handler
|
||||
if hasattr(route_handler, 'handlers'):
|
||||
route_handler = route_handler.handlers[method]
|
||||
return route_handler, [], kwargs
|
||||
|
||||
260
sanic/sanic.py
260
sanic/sanic.py
@@ -3,11 +3,15 @@ from asyncio import get_event_loop
|
||||
from collections import deque
|
||||
from functools import partial
|
||||
from inspect import isawaitable, stack, getmodulename
|
||||
import re
|
||||
from traceback import format_exc
|
||||
from urllib.parse import urlencode, urlunparse
|
||||
import warnings
|
||||
|
||||
from .config import Config
|
||||
from .constants import HTTP_METHODS
|
||||
from .exceptions import Handler
|
||||
from .exceptions import ServerError
|
||||
from .exceptions import ServerError, URLBuildError
|
||||
from .log import log
|
||||
from .response import HTTPResponse
|
||||
from .router import Router
|
||||
@@ -90,7 +94,10 @@ class Sanic:
|
||||
def patch(self, uri, host=None):
|
||||
return self.route(uri, methods=["PATCH"], host=host)
|
||||
|
||||
def add_route(self, handler, uri, methods=None, host=None):
|
||||
def delete(self, uri, host=None):
|
||||
return self.route(uri, methods=["DELETE"], host=host)
|
||||
|
||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None):
|
||||
"""
|
||||
A helper method to register class instance or
|
||||
functions as a handler to the application url
|
||||
@@ -98,9 +105,13 @@ class Sanic:
|
||||
|
||||
:param handler: function or class instance
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed
|
||||
:param methods: list or tuple of methods allowed, these are overridden
|
||||
if using a HTTPMethodView
|
||||
:return: function or class instance
|
||||
"""
|
||||
# Handle HTTPMethodView differently
|
||||
if hasattr(handler, 'view_class'):
|
||||
methods = frozenset(HTTP_METHODS)
|
||||
self.route(uri=uri, methods=methods, host=host)(handler)
|
||||
return handler
|
||||
|
||||
@@ -175,11 +186,97 @@ class Sanic:
|
||||
|
||||
def register_blueprint(self, *args, **kwargs):
|
||||
# TODO: deprecate 1.0
|
||||
log.warning("Use of register_blueprint will be deprecated in "
|
||||
"version 1.0. Please use the blueprint method instead",
|
||||
DeprecationWarning)
|
||||
if self.debug:
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("Use of register_blueprint will be deprecated in "
|
||||
"version 1.0. Please use the blueprint method"
|
||||
" instead",
|
||||
DeprecationWarning)
|
||||
return self.blueprint(*args, **kwargs)
|
||||
|
||||
def url_for(self, view_name: str, **kwargs):
|
||||
"""Builds a URL based on a view name and the values provided.
|
||||
|
||||
In order to build a URL, all request parameters must be supplied as
|
||||
keyword arguments, and each parameter must pass the test for the
|
||||
specified parameter type. If these conditions are not met, a
|
||||
`URLBuildError` will be thrown.
|
||||
|
||||
Keyword arguments that are not request parameters will be included in
|
||||
the output URL's query string.
|
||||
|
||||
:param view_name: A string referencing the view name
|
||||
:param **kwargs: keys and values that are used to build request
|
||||
parameters and query string arguments.
|
||||
|
||||
:return: the built URL
|
||||
|
||||
Raises:
|
||||
URLBuildError
|
||||
"""
|
||||
# find the route by the supplied view name
|
||||
uri, route = self.router.find_route_by_view_name(view_name)
|
||||
|
||||
if not uri or not route:
|
||||
raise URLBuildError(
|
||||
'Endpoint with name `{}` was not found'.format(
|
||||
view_name))
|
||||
|
||||
out = uri
|
||||
|
||||
# find all the parameters we will need to build in the URL
|
||||
matched_params = re.findall(
|
||||
self.router.parameter_pattern, uri)
|
||||
|
||||
for match in matched_params:
|
||||
name, _type, pattern = self.router.parse_parameter_string(
|
||||
match)
|
||||
# we only want to match against each individual parameter
|
||||
specific_pattern = '^{}$'.format(pattern)
|
||||
supplied_param = None
|
||||
|
||||
if kwargs.get(name):
|
||||
supplied_param = kwargs.get(name)
|
||||
del kwargs[name]
|
||||
else:
|
||||
raise URLBuildError(
|
||||
'Required parameter `{}` was not passed to url_for'.format(
|
||||
name))
|
||||
|
||||
supplied_param = str(supplied_param)
|
||||
# determine if the parameter supplied by the caller passes the test
|
||||
# in the URL
|
||||
passes_pattern = re.match(specific_pattern, supplied_param)
|
||||
|
||||
if not passes_pattern:
|
||||
if _type != str:
|
||||
msg = (
|
||||
'Value "{}" for parameter `{}` does not '
|
||||
'match pattern for type `{}`: {}'.format(
|
||||
supplied_param, name, _type.__name__, pattern))
|
||||
else:
|
||||
msg = (
|
||||
'Value "{}" for parameter `{}` '
|
||||
'does not satisfy pattern {}'.format(
|
||||
supplied_param, name, pattern))
|
||||
raise URLBuildError(msg)
|
||||
|
||||
# replace the parameter in the URL with the supplied value
|
||||
replacement_regex = '(<{}.*?>)'.format(name)
|
||||
|
||||
out = re.sub(
|
||||
replacement_regex, supplied_param, out)
|
||||
|
||||
# parse the remainder of the keyword arguments into a querystring
|
||||
if kwargs:
|
||||
query_string = urlencode(kwargs)
|
||||
out = urlunparse((
|
||||
'', '', out,
|
||||
'', query_string, ''
|
||||
))
|
||||
|
||||
return out
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Request Handling
|
||||
# -------------------------------------------------------------------- #
|
||||
@@ -294,13 +391,71 @@ class Sanic:
|
||||
:param protocol: Subclass of asyncio protocol class
|
||||
:return: Nothing
|
||||
"""
|
||||
server_settings = self._helper(
|
||||
host=host, port=port, debug=debug, before_start=before_start,
|
||||
after_start=after_start, before_stop=before_stop,
|
||||
after_stop=after_stop, ssl=ssl, sock=sock, workers=workers,
|
||||
loop=loop, protocol=protocol, backlog=backlog,
|
||||
stop_event=stop_event, register_sys_signals=register_sys_signals)
|
||||
try:
|
||||
if workers == 1:
|
||||
serve(**server_settings)
|
||||
else:
|
||||
serve_multiple(server_settings, workers, stop_event)
|
||||
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
'Experienced exception while trying to serve')
|
||||
|
||||
log.info("Server Stopped")
|
||||
|
||||
def stop(self):
|
||||
"""This kills the Sanic"""
|
||||
get_event_loop().stop()
|
||||
|
||||
async def create_server(self, host="127.0.0.1", port=8000, debug=False,
|
||||
before_start=None, after_start=None,
|
||||
before_stop=None, after_stop=None, ssl=None,
|
||||
sock=None, loop=None, protocol=HttpProtocol,
|
||||
backlog=100, stop_event=None):
|
||||
"""
|
||||
Asynchronous version of `run`.
|
||||
"""
|
||||
server_settings = self._helper(
|
||||
host=host, port=port, debug=debug, before_start=before_start,
|
||||
after_start=after_start, before_stop=before_stop,
|
||||
after_stop=after_stop, ssl=ssl, sock=sock, loop=loop,
|
||||
protocol=protocol, backlog=backlog, stop_event=stop_event,
|
||||
run_async=True)
|
||||
|
||||
# Serve
|
||||
proto = "http"
|
||||
if ssl is not None:
|
||||
proto = "https"
|
||||
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
|
||||
return await serve(**server_settings)
|
||||
|
||||
def _helper(self, host="127.0.0.1", port=8000, debug=False,
|
||||
before_start=None, after_start=None, before_stop=None,
|
||||
after_stop=None, ssl=None, sock=None, workers=1, loop=None,
|
||||
protocol=HttpProtocol, backlog=100, stop_event=None,
|
||||
register_sys_signals=True, run_async=False):
|
||||
"""
|
||||
Helper function used by `run` and `create_server`.
|
||||
"""
|
||||
|
||||
if loop is not None:
|
||||
if debug:
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("Passing a loop will be deprecated in version"
|
||||
" 0.4.0 https://github.com/channelcat/sanic/"
|
||||
"pull/335 has more information.",
|
||||
DeprecationWarning)
|
||||
|
||||
self.error_handler.debug = debug
|
||||
self.debug = debug
|
||||
if loop is not None:
|
||||
log.warning("Passing a loop will be deprecated in version 0.4.0"
|
||||
" https://github.com/channelcat/sanic/pull/335"
|
||||
" has more information.", DeprecationWarning)
|
||||
self.loop = loop
|
||||
self.loop = loop = get_event_loop()
|
||||
|
||||
server_settings = {
|
||||
'protocol': protocol,
|
||||
@@ -313,6 +468,7 @@ class Sanic:
|
||||
'error_handler': self.error_handler,
|
||||
'request_timeout': self.config.REQUEST_TIMEOUT,
|
||||
'request_max_size': self.config.REQUEST_MAX_SIZE,
|
||||
'loop': loop,
|
||||
'register_sys_signals': register_sys_signals,
|
||||
'backlog': backlog
|
||||
}
|
||||
@@ -342,7 +498,11 @@ class Sanic:
|
||||
|
||||
if debug:
|
||||
log.setLevel(logging.DEBUG)
|
||||
log.debug(self.config.LOGO)
|
||||
if self.config.LOGO is not None:
|
||||
log.debug(self.config.LOGO)
|
||||
|
||||
if run_async:
|
||||
server_settings['run_async'] = True
|
||||
|
||||
# Serve
|
||||
proto = "http"
|
||||
@@ -350,78 +510,4 @@ class Sanic:
|
||||
proto = "https"
|
||||
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
|
||||
try:
|
||||
if workers == 1:
|
||||
serve(**server_settings)
|
||||
else:
|
||||
serve_multiple(server_settings, workers, stop_event)
|
||||
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
'Experienced exception while trying to serve')
|
||||
|
||||
log.info("Server Stopped")
|
||||
|
||||
def stop(self):
|
||||
"""This kills the Sanic"""
|
||||
get_event_loop().stop()
|
||||
|
||||
async def create_server(self, host="127.0.0.1", port=8000, debug=False,
|
||||
before_start=None, after_start=None,
|
||||
before_stop=None, after_stop=None, ssl=None,
|
||||
sock=None, loop=None, protocol=HttpProtocol,
|
||||
backlog=100, stop_event=None):
|
||||
"""
|
||||
Asynchronous version of `run`.
|
||||
"""
|
||||
if loop is not None:
|
||||
log.warning("Passing a loop will be deprecated in version 0.4.0"
|
||||
" https://github.com/channelcat/sanic/pull/335"
|
||||
" has more information.", DeprecationWarning)
|
||||
loop = get_event_loop()
|
||||
server_settings = {
|
||||
'protocol': protocol,
|
||||
'host': host,
|
||||
'port': port,
|
||||
'sock': sock,
|
||||
'ssl': ssl,
|
||||
'debug': debug,
|
||||
'request_handler': self.handle_request,
|
||||
'error_handler': self.error_handler,
|
||||
'request_timeout': self.config.REQUEST_TIMEOUT,
|
||||
'request_max_size': self.config.REQUEST_MAX_SIZE,
|
||||
'loop': loop,
|
||||
'backlog': backlog
|
||||
}
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Register start/stop events
|
||||
# -------------------------------------------- #
|
||||
|
||||
for event_name, settings_name, args, reverse in (
|
||||
("before_server_start", "before_start", before_start, False),
|
||||
("after_server_start", "after_start", after_start, False),
|
||||
("before_server_stop", "before_stop", before_stop, True),
|
||||
("after_server_stop", "after_stop", after_stop, True)):
|
||||
listeners = []
|
||||
for blueprint in self.blueprints.values():
|
||||
listeners += blueprint.listeners[event_name]
|
||||
if args:
|
||||
if callable(args):
|
||||
args = [args]
|
||||
listeners += args
|
||||
if reverse:
|
||||
listeners.reverse()
|
||||
# Prepend sanic to the arguments when listeners are triggered
|
||||
listeners = [partial(listener, self) for listener in listeners]
|
||||
server_settings[settings_name] = listeners
|
||||
|
||||
server_settings['run_async'] = True
|
||||
|
||||
# Serve
|
||||
proto = "http"
|
||||
if ssl is not None:
|
||||
proto = "https"
|
||||
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
|
||||
return await serve(**server_settings)
|
||||
return server_settings
|
||||
|
||||
@@ -9,6 +9,7 @@ from signal import SIGTERM, SIGINT
|
||||
from signal import signal as signal_func
|
||||
from socket import socket, SOL_SOCKET, SO_REUSEADDR
|
||||
from time import time
|
||||
import warnings
|
||||
|
||||
from httptools import HttpRequestParser
|
||||
from httptools.parser.errors import HttpParserError
|
||||
@@ -296,8 +297,9 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
:param protocol: Subclass of asyncio protocol class
|
||||
:return: Nothing
|
||||
"""
|
||||
loop = async_loop.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
if not run_async:
|
||||
loop = async_loop.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
if debug:
|
||||
loop.set_debug(debug)
|
||||
@@ -384,9 +386,11 @@ def serve_multiple(server_settings, workers, stop_event=None):
|
||||
:return:
|
||||
"""
|
||||
if server_settings.get('loop', None) is not None:
|
||||
log.warning("Passing a loop will be deprecated in version 0.4.0"
|
||||
" https://github.com/channelcat/sanic/pull/335"
|
||||
" has more information.", DeprecationWarning)
|
||||
if server_settings.get('debug', False):
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("Passing a loop will be deprecated in version 0.4.0"
|
||||
" https://github.com/channelcat/sanic/pull/335"
|
||||
" has more information.", DeprecationWarning)
|
||||
server_settings['reuse_port'] = True
|
||||
|
||||
sock = socket()
|
||||
|
||||
@@ -9,7 +9,8 @@ async def local_request(method, uri, cookies=None, *args, **kwargs):
|
||||
url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri)
|
||||
log.info(url)
|
||||
async with aiohttp.ClientSession(cookies=cookies) as session:
|
||||
async with getattr(session, method)(url, *args, **kwargs) as response:
|
||||
async with getattr(
|
||||
session, method.lower())(url, *args, **kwargs) as response:
|
||||
response.text = await response.text()
|
||||
response.body = await response.read()
|
||||
return response
|
||||
@@ -18,19 +19,20 @@ async def local_request(method, uri, cookies=None, *args, **kwargs):
|
||||
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||
debug=False, server_kwargs={},
|
||||
*request_args, **request_kwargs):
|
||||
results = []
|
||||
results = [None, None]
|
||||
exceptions = []
|
||||
|
||||
if gather_request:
|
||||
def _collect_request(request):
|
||||
results.append(request)
|
||||
if results[0] is None:
|
||||
results[0] = request
|
||||
app.request_middleware.appendleft(_collect_request)
|
||||
|
||||
async def _collect_response(sanic, loop):
|
||||
try:
|
||||
response = await local_request(method, uri, *request_args,
|
||||
**request_kwargs)
|
||||
results.append(response)
|
||||
results[-1] = response
|
||||
except Exception as e:
|
||||
exceptions.append(e)
|
||||
app.stop()
|
||||
@@ -51,7 +53,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||
results))
|
||||
else:
|
||||
try:
|
||||
return results[0]
|
||||
return results[-1]
|
||||
except:
|
||||
raise ValueError(
|
||||
"Request object expected, got ({})".format(results))
|
||||
|
||||
@@ -64,6 +64,7 @@ class HTTPMethodView:
|
||||
view.view_class = cls
|
||||
view.__doc__ = cls.__doc__
|
||||
view.__module__ = cls.__module__
|
||||
view.__name__ = cls.__name__
|
||||
return view
|
||||
|
||||
|
||||
|
||||
2
setup.py
2
setup.py
@@ -16,7 +16,7 @@ with codecs.open(os.path.join(os.path.abspath(os.path.dirname(
|
||||
raise RuntimeError('Unable to determine version.')
|
||||
|
||||
setup(
|
||||
name='Sanic',
|
||||
name='sanic',
|
||||
version=version,
|
||||
url='http://github.com/channelcat/sanic/',
|
||||
license='MIT',
|
||||
|
||||
@@ -228,3 +228,79 @@ def test_bp_static():
|
||||
request, response = sanic_endpoint_test(app, uri='/testing.file')
|
||||
assert response.status == 200
|
||||
assert response.body == current_file_contents
|
||||
|
||||
def test_bp_shorthand():
|
||||
app = Sanic('test_shorhand_routes')
|
||||
blueprint = Blueprint('test_shorhand_routes')
|
||||
|
||||
@blueprint.get('/get')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@blueprint.put('/put')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@blueprint.post('/post')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@blueprint.head('/head')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@blueprint.options('/options')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@blueprint.patch('/patch')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@blueprint.delete('/delete')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/options', method='options')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/options', method='get')
|
||||
assert response.status == 405
|
||||
|
||||
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
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/delete', method='delete')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/delete', method='get')
|
||||
assert response.status == 405
|
||||
|
||||
76
tests/test_config.py
Normal file
76
tests/test_config.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from os import environ
|
||||
import pytest
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
def test_load_from_object():
|
||||
app = Sanic('test_load_from_object')
|
||||
class Config:
|
||||
not_for_config = 'should not be used'
|
||||
CONFIG_VALUE = 'should be used'
|
||||
|
||||
app.config.from_object(Config)
|
||||
assert 'CONFIG_VALUE' in app.config
|
||||
assert app.config.CONFIG_VALUE == 'should be used'
|
||||
assert 'not_for_config' not in app.config
|
||||
|
||||
|
||||
def test_load_from_file():
|
||||
app = Sanic('test_load_from_file')
|
||||
config = b"""
|
||||
VALUE = 'some value'
|
||||
condition = 1 == 1
|
||||
if condition:
|
||||
CONDITIONAL = 'should be set'
|
||||
"""
|
||||
with NamedTemporaryFile() as config_file:
|
||||
config_file.write(config)
|
||||
config_file.seek(0)
|
||||
app.config.from_pyfile(config_file.name)
|
||||
assert 'VALUE' in app.config
|
||||
assert app.config.VALUE == 'some value'
|
||||
assert 'CONDITIONAL' in app.config
|
||||
assert app.config.CONDITIONAL == 'should be set'
|
||||
assert 'condition' not in app.config
|
||||
|
||||
|
||||
def test_load_from_missing_file():
|
||||
app = Sanic('test_load_from_missing_file')
|
||||
with pytest.raises(IOError):
|
||||
app.config.from_pyfile('non-existent file')
|
||||
|
||||
|
||||
def test_load_from_envvar():
|
||||
app = Sanic('test_load_from_envvar')
|
||||
config = b"VALUE = 'some value'"
|
||||
with NamedTemporaryFile() as config_file:
|
||||
config_file.write(config)
|
||||
config_file.seek(0)
|
||||
environ['APP_CONFIG'] = config_file.name
|
||||
app.config.from_envvar('APP_CONFIG')
|
||||
assert 'VALUE' in app.config
|
||||
assert app.config.VALUE == 'some value'
|
||||
|
||||
|
||||
def test_load_from_missing_envvar():
|
||||
app = Sanic('test_load_from_missing_envvar')
|
||||
with pytest.raises(RuntimeError):
|
||||
app.config.from_envvar('non-existent variable')
|
||||
|
||||
|
||||
def test_overwrite_exisiting_config():
|
||||
app = Sanic('test_overwrite_exisiting_config')
|
||||
app.config.DEFAULT = 1
|
||||
class Config:
|
||||
DEFAULT = 2
|
||||
|
||||
app.config.from_object(Config)
|
||||
assert app.config.DEFAULT == 2
|
||||
|
||||
|
||||
def test_missing_config():
|
||||
app = Sanic('test_missing_config')
|
||||
with pytest.raises(AttributeError):
|
||||
app.config.NON_EXISTENT
|
||||
46
tests/test_dynamic_routes.py
Normal file
46
tests/test_dynamic_routes.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.router import RouteExists
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method,attr, expected", [
|
||||
("get", "text", "OK1 test"),
|
||||
("post", "text", "OK2 test"),
|
||||
("put", "text", "OK2 test"),
|
||||
("delete", "status", 405),
|
||||
])
|
||||
def test_overload_dynamic_routes(method, attr, expected):
|
||||
app = Sanic('test_dynamic_route')
|
||||
|
||||
@app.route('/overload/<param>', methods=['GET'])
|
||||
async def handler1(request, param):
|
||||
return text('OK1 ' + param)
|
||||
|
||||
@app.route('/overload/<param>', methods=['POST', 'PUT'])
|
||||
async def handler2(request, param):
|
||||
return text('OK2 ' + param)
|
||||
|
||||
request, response = sanic_endpoint_test(
|
||||
app, method, uri='/overload/test')
|
||||
assert getattr(response, attr) == expected
|
||||
|
||||
|
||||
def test_overload_dynamic_routes_exist():
|
||||
app = Sanic('test_dynamic_route')
|
||||
|
||||
@app.route('/overload/<param>', methods=['GET'])
|
||||
async def handler1(request, param):
|
||||
return text('OK1 ' + param)
|
||||
|
||||
@app.route('/overload/<param>', methods=['POST', 'PUT'])
|
||||
async def handler2(request, param):
|
||||
return text('OK2 ' + param)
|
||||
|
||||
# if this doesn't raise an error, than at least the below should happen:
|
||||
# assert response.text == 'Duplicated'
|
||||
with pytest.raises(RouteExists):
|
||||
@app.route('/overload/<param>', methods=['PUT', 'DELETE'])
|
||||
async def handler3(request):
|
||||
return text('Duplicated')
|
||||
@@ -28,6 +28,13 @@ def handler_4(request):
|
||||
return text(foo)
|
||||
|
||||
|
||||
@exception_handler_app.route('/5')
|
||||
def handler_5(request):
|
||||
class CustomServerError(ServerError):
|
||||
pass
|
||||
raise CustomServerError('Custom server error')
|
||||
|
||||
|
||||
@exception_handler_app.exception(NotFound, ServerError)
|
||||
def handler_exception(request, exception):
|
||||
return text("OK")
|
||||
@@ -71,3 +78,8 @@ def test_html_traceback_output_in_debug_mode():
|
||||
assert (
|
||||
"NameError: name 'bar' "
|
||||
"is not defined while handling uri /4") == summary_text
|
||||
|
||||
|
||||
def test_inherited_exception_handler():
|
||||
request, response = sanic_endpoint_test(exception_handler_app, uri='/5')
|
||||
assert response.status == 200
|
||||
|
||||
96
tests/test_redirect.py
Normal file
96
tests/test_redirect.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text, redirect
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
|
||||
@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')
|
||||
|
||||
@app.route('/1')
|
||||
def handler(request):
|
||||
return redirect('/2')
|
||||
|
||||
@app.route('/2')
|
||||
def handler(request):
|
||||
return redirect('/3')
|
||||
|
||||
@app.route('/3')
|
||||
def handler(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'
|
||||
|
||||
|
||||
def test_chained_redirect(redirect_app):
|
||||
"""Test sanic_endpoint_test is working for redirection"""
|
||||
request, response = sanic_endpoint_test(redirect_app, uri='/1')
|
||||
assert request.url.endswith('/1')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
assert response.url.endswith('/3')
|
||||
@@ -58,7 +58,7 @@ def test_non_str_headers():
|
||||
request, response = sanic_endpoint_test(app)
|
||||
|
||||
assert response.headers.get('answer') == '42'
|
||||
|
||||
|
||||
def test_invalid_response():
|
||||
app = Sanic('test_invalid_response')
|
||||
|
||||
@@ -73,8 +73,8 @@ def test_invalid_response():
|
||||
request, response = sanic_endpoint_test(app)
|
||||
assert response.status == 500
|
||||
assert response.text == "Internal Server Error."
|
||||
|
||||
|
||||
|
||||
|
||||
def test_json():
|
||||
app = Sanic('test_json')
|
||||
|
||||
@@ -189,73 +189,3 @@ def test_post_form_multipart_form_data():
|
||||
request, response = sanic_endpoint_test(app, data=payload, headers=headers)
|
||||
|
||||
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'
|
||||
|
||||
@@ -16,47 +16,35 @@ def static_file_directory():
|
||||
return static_directory
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def static_file_path(static_file_directory):
|
||||
"""The path to the static file that we want to serve"""
|
||||
return os.path.join(static_file_directory, 'test.file')
|
||||
def get_file_path(static_file_directory, file_name):
|
||||
return os.path.join(static_file_directory, file_name)
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def static_file_content(static_file_path):
|
||||
def get_file_content(static_file_directory, file_name):
|
||||
"""The content of the static file to check"""
|
||||
with open(static_file_path, 'rb') as file:
|
||||
with open(get_file_path(static_file_directory, file_name), 'rb') as file:
|
||||
return file.read()
|
||||
|
||||
|
||||
def test_static_file(static_file_path, static_file_content):
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_file(static_file_directory, file_name):
|
||||
app = Sanic('test_static')
|
||||
app.static('/testing.file', static_file_path)
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name))
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/testing.file')
|
||||
assert response.status == 200
|
||||
assert response.body == static_file_content
|
||||
assert response.body == get_file_content(static_file_directory, file_name)
|
||||
|
||||
|
||||
def test_static_directory(
|
||||
static_file_directory, static_file_path, static_file_content):
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
@pytest.mark.parametrize('base_uri', ['/static', '', '/dir'])
|
||||
def test_static_directory(file_name, base_uri, static_file_directory):
|
||||
|
||||
app = Sanic('test_static')
|
||||
app.static('/dir', static_file_directory)
|
||||
app.static(base_uri, static_file_directory)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/dir/test.file')
|
||||
request, response = sanic_endpoint_test(
|
||||
app, uri='{}/{}'.format(base_uri, file_name))
|
||||
assert response.status == 200
|
||||
assert response.body == static_file_content
|
||||
|
||||
|
||||
def test_static_url_decode_file(static_file_directory):
|
||||
decode_me_path = os.path.join(static_file_directory, 'decode me.txt')
|
||||
with open(decode_me_path, 'rb') as file:
|
||||
decode_me_contents = file.read()
|
||||
|
||||
app = Sanic('test_static')
|
||||
app.static('/dir', static_file_directory)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/dir/decode me.txt')
|
||||
assert response.status == 200
|
||||
assert response.body == decode_me_contents
|
||||
assert response.body == get_file_content(static_file_directory, file_name)
|
||||
|
||||
261
tests/test_url_building.py
Normal file
261
tests/test_url_building.py
Normal file
@@ -0,0 +1,261 @@
|
||||
import pytest as pytest
|
||||
from urllib.parse import urlsplit, parse_qsl
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.exceptions import URLBuildError
|
||||
|
||||
import string
|
||||
|
||||
|
||||
def _generate_handlers_from_names(app, l):
|
||||
for name in l:
|
||||
# this is the easiest way to generate functions with dynamic names
|
||||
exec('@app.route(name)\ndef {}(request):\n\treturn text("{}")'.format(
|
||||
name, name))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def simple_app():
|
||||
app = Sanic('simple_app')
|
||||
handler_names = list(string.ascii_letters)
|
||||
|
||||
_generate_handlers_from_names(app, handler_names)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def test_simple_url_for_getting(simple_app):
|
||||
for letter in string.ascii_letters:
|
||||
url = simple_app.url_for(letter)
|
||||
|
||||
assert url == '/{}'.format(letter)
|
||||
request, response = sanic_endpoint_test(
|
||||
simple_app, uri=url)
|
||||
assert response.status == 200
|
||||
assert response.text == letter
|
||||
|
||||
|
||||
def test_fails_if_endpoint_not_found():
|
||||
app = Sanic('fail_url_build')
|
||||
|
||||
@app.route('/fail')
|
||||
def fail():
|
||||
return text('this should fail')
|
||||
|
||||
with pytest.raises(URLBuildError) as e:
|
||||
app.url_for('passes')
|
||||
|
||||
assert str(e.value) == 'Endpoint with name `passes` was not found'
|
||||
|
||||
|
||||
def test_fails_url_build_if_param_not_passed():
|
||||
url = '/'
|
||||
|
||||
for letter in string.ascii_letters:
|
||||
url += '<{}>/'.format(letter)
|
||||
|
||||
app = Sanic('fail_url_build')
|
||||
|
||||
@app.route(url)
|
||||
def fail():
|
||||
return text('this should fail')
|
||||
|
||||
fail_args = list(string.ascii_letters)
|
||||
fail_args.pop()
|
||||
|
||||
fail_kwargs = {l: l for l in fail_args}
|
||||
|
||||
with pytest.raises(URLBuildError) as e:
|
||||
app.url_for('fail', **fail_kwargs)
|
||||
|
||||
assert 'Required parameter `Z` was not passed to url_for' in str(e.value)
|
||||
|
||||
|
||||
COMPLEX_PARAM_URL = (
|
||||
'/<foo:int>/<four_letter_string:[A-z]{4}>/'
|
||||
'<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:number>')
|
||||
PASSING_KWARGS = {
|
||||
'foo': 4, 'four_letter_string': 'woof',
|
||||
'two_letter_string': 'ba', 'normal_string': 'normal',
|
||||
'some_number': '1.001'}
|
||||
EXPECTED_BUILT_URL = '/4/woof/ba/normal/1.001'
|
||||
|
||||
|
||||
def test_fails_with_int_message():
|
||||
app = Sanic('fail_url_build')
|
||||
|
||||
@app.route(COMPLEX_PARAM_URL)
|
||||
def fail():
|
||||
return text('this should fail')
|
||||
|
||||
failing_kwargs = dict(PASSING_KWARGS)
|
||||
failing_kwargs['foo'] = 'not_int'
|
||||
|
||||
with pytest.raises(URLBuildError) as e:
|
||||
app.url_for('fail', **failing_kwargs)
|
||||
|
||||
expected_error = (
|
||||
'Value "not_int" for parameter `foo` '
|
||||
'does not match pattern for type `int`: \d+')
|
||||
assert str(e.value) == expected_error
|
||||
|
||||
|
||||
def test_fails_with_two_letter_string_message():
|
||||
app = Sanic('fail_url_build')
|
||||
|
||||
@app.route(COMPLEX_PARAM_URL)
|
||||
def fail():
|
||||
return text('this should fail')
|
||||
|
||||
failing_kwargs = dict(PASSING_KWARGS)
|
||||
failing_kwargs['two_letter_string'] = 'foobar'
|
||||
|
||||
with pytest.raises(URLBuildError) as e:
|
||||
app.url_for('fail', **failing_kwargs)
|
||||
|
||||
expected_error = (
|
||||
'Value "foobar" for parameter `two_letter_string` '
|
||||
'does not satisfy pattern [A-z]{2}')
|
||||
|
||||
assert str(e.value) == expected_error
|
||||
|
||||
|
||||
def test_fails_with_number_message():
|
||||
app = Sanic('fail_url_build')
|
||||
|
||||
@app.route(COMPLEX_PARAM_URL)
|
||||
def fail():
|
||||
return text('this should fail')
|
||||
|
||||
failing_kwargs = dict(PASSING_KWARGS)
|
||||
failing_kwargs['some_number'] = 'foo'
|
||||
|
||||
with pytest.raises(URLBuildError) as e:
|
||||
app.url_for('fail', **failing_kwargs)
|
||||
|
||||
expected_error = (
|
||||
'Value "foo" for parameter `some_number` '
|
||||
'does not match pattern for type `float`: [0-9\\\\.]+')
|
||||
|
||||
assert str(e.value) == expected_error
|
||||
|
||||
|
||||
def test_adds_other_supplied_values_as_query_string():
|
||||
app = Sanic('passes')
|
||||
|
||||
@app.route(COMPLEX_PARAM_URL)
|
||||
def passes():
|
||||
return text('this should pass')
|
||||
|
||||
new_kwargs = dict(PASSING_KWARGS)
|
||||
new_kwargs['added_value_one'] = 'one'
|
||||
new_kwargs['added_value_two'] = 'two'
|
||||
|
||||
url = app.url_for('passes', **new_kwargs)
|
||||
|
||||
query = dict(parse_qsl(urlsplit(url).query))
|
||||
|
||||
assert query['added_value_one'] == 'one'
|
||||
assert query['added_value_two'] == 'two'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def blueprint_app():
|
||||
app = Sanic('blueprints')
|
||||
|
||||
first_print = Blueprint('first', url_prefix='/first')
|
||||
second_print = Blueprint('second', url_prefix='/second')
|
||||
|
||||
@first_print.route('/foo')
|
||||
def foo():
|
||||
return text('foo from first')
|
||||
|
||||
@first_print.route('/foo/<param>')
|
||||
def foo_with_param(request, param):
|
||||
return text(
|
||||
'foo from first : {}'.format(param))
|
||||
|
||||
@second_print.route('/foo') # noqa
|
||||
def foo():
|
||||
return text('foo from second')
|
||||
|
||||
@second_print.route('/foo/<param>') # noqa
|
||||
def foo_with_param(request, param):
|
||||
return text(
|
||||
'foo from second : {}'.format(param))
|
||||
|
||||
app.blueprint(first_print)
|
||||
app.blueprint(second_print)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def test_blueprints_are_named_correctly(blueprint_app):
|
||||
first_url = blueprint_app.url_for('first.foo')
|
||||
assert first_url == '/first/foo'
|
||||
|
||||
second_url = blueprint_app.url_for('second.foo')
|
||||
assert second_url == '/second/foo'
|
||||
|
||||
|
||||
def test_blueprints_work_with_params(blueprint_app):
|
||||
first_url = blueprint_app.url_for('first.foo_with_param', param='bar')
|
||||
assert first_url == '/first/foo/bar'
|
||||
|
||||
second_url = blueprint_app.url_for('second.foo_with_param', param='bar')
|
||||
assert second_url == '/second/foo/bar'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def methodview_app():
|
||||
app = Sanic('methodview')
|
||||
|
||||
class ViewOne(HTTPMethodView):
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
def post(self, request):
|
||||
return text('I am post method')
|
||||
|
||||
def put(self, request):
|
||||
return text('I am put method')
|
||||
|
||||
def patch(self, request):
|
||||
return text('I am patch method')
|
||||
|
||||
def delete(self, request):
|
||||
return text('I am delete method')
|
||||
|
||||
app.add_route(ViewOne.as_view('view_one'), '/view_one')
|
||||
|
||||
class ViewTwo(HTTPMethodView):
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
def post(self, request):
|
||||
return text('I am post method')
|
||||
|
||||
def put(self, request):
|
||||
return text('I am put method')
|
||||
|
||||
def patch(self, request):
|
||||
return text('I am patch method')
|
||||
|
||||
def delete(self, request):
|
||||
return text('I am delete method')
|
||||
|
||||
app.add_route(ViewTwo.as_view(), '/view_two')
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def test_methodview_naming(methodview_app):
|
||||
viewone_url = methodview_app.url_for('ViewOne')
|
||||
viewtwo_url = methodview_app.url_for('ViewTwo')
|
||||
|
||||
assert viewone_url == '/view_one'
|
||||
assert viewtwo_url == '/view_two'
|
||||
@@ -1,43 +1,45 @@
|
||||
import pytest as pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text, HTTPResponse
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.request import Request
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.constants import HTTP_METHODS
|
||||
|
||||
|
||||
def test_methods():
|
||||
@pytest.mark.parametrize('method', HTTP_METHODS)
|
||||
def test_methods(method):
|
||||
app = Sanic('test_methods')
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
return text('', headers={'method': 'GET'})
|
||||
|
||||
def post(self, request):
|
||||
return text('I am post method')
|
||||
return text('', headers={'method': 'POST'})
|
||||
|
||||
def put(self, request):
|
||||
return text('I am put method')
|
||||
return text('', headers={'method': 'PUT'})
|
||||
|
||||
def head(self, request):
|
||||
return text('', headers={'method': 'HEAD'})
|
||||
|
||||
def options(self, request):
|
||||
return text('', headers={'method': 'OPTIONS'})
|
||||
|
||||
def patch(self, request):
|
||||
return text('I am patch method')
|
||||
return text('', headers={'method': 'PATCH'})
|
||||
|
||||
def delete(self, request):
|
||||
return text('I am delete method')
|
||||
return text('', headers={'method': 'DELETE'})
|
||||
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
|
||||
request, response = sanic_endpoint_test(app, method="get")
|
||||
assert response.text == 'I am get method'
|
||||
request, response = sanic_endpoint_test(app, method="post")
|
||||
assert response.text == 'I am post method'
|
||||
request, response = sanic_endpoint_test(app, method="put")
|
||||
assert response.text == 'I am put method'
|
||||
request, response = sanic_endpoint_test(app, method="patch")
|
||||
assert response.text == 'I am patch method'
|
||||
request, response = sanic_endpoint_test(app, method="delete")
|
||||
assert response.text == 'I am delete method'
|
||||
request, response = sanic_endpoint_test(app, method=method)
|
||||
assert response.headers['method'] == method
|
||||
|
||||
|
||||
def test_unexisting_methods():
|
||||
|
||||
Reference in New Issue
Block a user