Compare commits
217 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfbcdf8b86 | ||
|
|
42e1d18470 | ||
|
|
435dc72aa1 | ||
|
|
66c380548b | ||
|
|
13f81e9a6f | ||
|
|
68c8796adb | ||
|
|
4232f5342e | ||
|
|
28c31359bf | ||
|
|
a36e815227 | ||
|
|
ff3d33d5e0 | ||
|
|
d015d6b103 | ||
|
|
0d160fbeaa | ||
|
|
fedd7920a8 | ||
|
|
6afac06596 | ||
|
|
91b2b40b9a | ||
|
|
56ecb6a3ea | ||
|
|
64f73f624f | ||
|
|
20b78b68a6 | ||
|
|
d1cc14201b | ||
|
|
a57ec9e669 | ||
|
|
9c72b557ec | ||
|
|
207ec1e032 | ||
|
|
ff5d4276bc | ||
|
|
26c9618e63 | ||
|
|
9c306899ba | ||
|
|
4b7e7aab33 | ||
|
|
2921e9f868 | ||
|
|
70b7606cb8 | ||
|
|
0cae91d525 | ||
|
|
2c2a28e46b | ||
|
|
c75d484f23 | ||
|
|
072f7fec30 | ||
|
|
a73379495f | ||
|
|
0b914866eb | ||
|
|
f12186d024 | ||
|
|
4a2835dc84 | ||
|
|
55dc45de33 | ||
|
|
39aa64ff68 | ||
|
|
94bb91f2fa | ||
|
|
f553ca95a5 | ||
|
|
b44e8167d7 | ||
|
|
36c1122d20 | ||
|
|
ad13529eaa | ||
|
|
04c8f4a5ed | ||
|
|
850446195f | ||
|
|
865506546f | ||
|
|
fe72fadedd | ||
|
|
9cf9d3ad5c | ||
|
|
426d0e6af1 | ||
|
|
483b442b7d | ||
|
|
9420b0c947 | ||
|
|
dcfb7345f0 | ||
|
|
f9ab24a077 | ||
|
|
f932d16ba7 | ||
|
|
7d9acc3c36 | ||
|
|
72f735124f | ||
|
|
6ea5a4719a | ||
|
|
f8c50b7f1e | ||
|
|
feb1f1d71a | ||
|
|
550afc27dc | ||
|
|
d13af4bdc4 | ||
|
|
53a365dd2b | ||
|
|
6726affa7e | ||
|
|
6ecf2a6eb2 | ||
|
|
a359e11f97 | ||
|
|
54b2d74068 | ||
|
|
c99aa7279b | ||
|
|
4fa568ce8a | ||
|
|
874698b93f | ||
|
|
b286fc1e4a | ||
|
|
1cf1024332 | ||
|
|
6b391b701b | ||
|
|
efc90f8f5a | ||
|
|
6535ba7c24 | ||
|
|
5c29c3d160 | ||
|
|
742d4bff78 | ||
|
|
7726ffa3f7 | ||
|
|
b442d78ebb | ||
|
|
d44edb5930 | ||
|
|
d5633b3705 | ||
|
|
3b68dc72e7 | ||
|
|
51611c3934 | ||
|
|
747b7567d7 | ||
|
|
797891d6cf | ||
|
|
286dc3c32b | ||
|
|
a66ba21c3d | ||
|
|
b139810b6a | ||
|
|
dddc18d77c | ||
|
|
56f56d008a | ||
|
|
81a8a99b6e | ||
|
|
07aa0ee7ad | ||
|
|
b66a6bddbc | ||
|
|
d57d90fe6b | ||
|
|
de6c646ee8 | ||
|
|
4839ede64f | ||
|
|
84f5faf653 | ||
|
|
281077bc26 | ||
|
|
ed5fe9ae9f | ||
|
|
1866e4ef44 | ||
|
|
a6a07c3b3a | ||
|
|
b2af8e640c | ||
|
|
7a3f5d508b | ||
|
|
7e1fd03104 | ||
|
|
758415d326 | ||
|
|
1660041470 | ||
|
|
1783df883e | ||
|
|
b2be821637 | ||
|
|
051ff2b325 | ||
|
|
4d6f9ffd7c | ||
|
|
d614823013 | ||
|
|
48aa51b739 | ||
|
|
41c6125e1b | ||
|
|
bb3d48f98b | ||
|
|
b5e46e83e2 | ||
|
|
2340910b46 | ||
|
|
d8c4c1525d | ||
|
|
6713ef7726 | ||
|
|
ae7555b065 | ||
|
|
ee6ff0cc60 | ||
|
|
94b2352c2c | ||
|
|
cf3f943feb | ||
|
|
55c4d583b9 | ||
|
|
0e1bb6ab04 | ||
|
|
7944cff7a5 | ||
|
|
8b08a370c5 | ||
|
|
2d5fd2fe1c | ||
|
|
b5e50ecb75 | ||
|
|
e00c9d0ee0 | ||
|
|
be9c9f045a | ||
|
|
75fca1b9c7 | ||
|
|
1436fb3ef4 | ||
|
|
2bfd127218 | ||
|
|
de5da63d5c | ||
|
|
fb419eaa36 | ||
|
|
cf2a363e5e | ||
|
|
7401facc21 | ||
|
|
579afe012b | ||
|
|
4f856e8783 | ||
|
|
eb059183f7 | ||
|
|
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 | ||
|
|
d193a1eb70 | ||
|
|
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,7 +1,7 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = sanic, tests
|
||||
omit = site-packages
|
||||
source = sanic
|
||||
omit = site-packages, sanic/utils.py
|
||||
|
||||
[html]
|
||||
directory = coverage
|
||||
directory = coverage
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,4 +12,5 @@ settings.py
|
||||
.cache/*
|
||||
.python-version
|
||||
docs/_build/
|
||||
docs/_api/
|
||||
docs/_api/
|
||||
build/*
|
||||
|
||||
@@ -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
|
||||
|
||||
26
README.rst
26
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
|
||||
@@ -93,10 +78,7 @@ Documentation can be found in the ``docs`` directory.
|
||||
TODO
|
||||
----
|
||||
* Streamed file processing
|
||||
* File output
|
||||
* Examples of integrations with 3rd-party modules
|
||||
* RESTful router
|
||||
|
||||
* http2
|
||||
Limitations
|
||||
-----------
|
||||
* No wheels for uvloop and httptools on Windows :(
|
||||
|
||||
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,12 +0,0 @@
|
||||
# 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.
|
||||
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
|
||||
|
||||
**Previous:** [Testing](testing.md)
|
||||
|
||||
**Next:** [Contributing](contributing.md)
|
||||
@@ -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
|
||||
111
docs/routing.md
111
docs/routing.md
@@ -1,111 +0,0 @@
|
||||
# Routing
|
||||
|
||||
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
|
||||
`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
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/tag/<tag>')
|
||||
async def tag_handler(request, 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>')
|
||||
async def integer_handler(request, integer_arg):
|
||||
return text('Integer - {}'.format(integer_arg))
|
||||
|
||||
@app.route('/number/<number_arg:number>')
|
||||
async def number_handler(request, number_arg):
|
||||
return text('Number - {}'.format(number_arg))
|
||||
|
||||
@app.route('/person/<name:[A-z]>')
|
||||
async def person_handler(request, name):
|
||||
return text('Person - {}'.format(name))
|
||||
|
||||
@app.route('/folder/<folder_id:[A-z0-9]{0,4}>')
|
||||
async def folder_handler(request, folder_id):
|
||||
return text('Folder - {}'.format(folder_id))
|
||||
|
||||
```
|
||||
|
||||
## HTTP request types
|
||||
|
||||
By default, a route defined on a URL will be used for all 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.
|
||||
|
||||
```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)
|
||||
@@ -131,8 +131,8 @@ can be used to implement our API versioning scheme.
|
||||
from sanic.response import text
|
||||
from sanic import Blueprint
|
||||
|
||||
blueprint_v1 = Blueprint('v1')
|
||||
blueprint_v2 = Blueprint('v2')
|
||||
blueprint_v1 = Blueprint('v1', url_prefix='/v1')
|
||||
blueprint_v2 = Blueprint('v2', url_prefix='/v2')
|
||||
|
||||
@blueprint_v1.route('/')
|
||||
async def api_v1_root(request):
|
||||
@@ -153,12 +153,27 @@ from sanic import Sanic
|
||||
from blueprints import blueprint_v1, blueprint_v2
|
||||
|
||||
app = Sanic(__name__)
|
||||
app.blueprint(blueprint_v1)
|
||||
app.blueprint(blueprint_v2)
|
||||
app.blueprint(blueprint_v1, url_prefix='/v1')
|
||||
app.blueprint(blueprint_v2, url_prefix='/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) # --> '/v1/post/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)
|
||||
@@ -7,15 +7,6 @@ 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.
|
||||
@@ -53,7 +44,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)
|
||||
14
docs/sanic/extensions.md
Normal file
14
docs/sanic/extensions.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# 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.
|
||||
- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress.
|
||||
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
|
||||
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
|
||||
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
|
||||
- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper.
|
||||
- [Sanic CRUD](https://github.com/Typhon66/sanic_crud): CRUD REST API generation with peewee models.
|
||||
- [UserAgent](https://github.com/lixxu/sanic-useragent): Add `user_agent` to request
|
||||
@@ -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)
|
||||
@@ -69,6 +69,23 @@ The following variables are accessible as properties on `Request` objects:
|
||||
|
||||
- `ip` (str) - IP address of the requester.
|
||||
|
||||
- `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object.
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
from sanic import Blueprint
|
||||
|
||||
bp = Blueprint('my_blueprint')
|
||||
|
||||
@bp.route('/')
|
||||
async def bp_root(request):
|
||||
if request.app.config['DEBUG']:
|
||||
return json({'status': 'debug'})
|
||||
else:
|
||||
return json({'status': 'production'})
|
||||
|
||||
```
|
||||
|
||||
## Accessing values using `get` and `getlist`
|
||||
|
||||
The request properties which return a dictionary actually return a subclass of
|
||||
@@ -89,7 +106,3 @@ args.get('titles') # => 'Post 1'
|
||||
|
||||
args.getlist('titles') # => ['Post 1', 'Post 2']
|
||||
```
|
||||
|
||||
**Previous:** [Routing](routing.md)
|
||||
|
||||
**Next:** [Deploying](deploying.md)
|
||||
183
docs/sanic/routing.md
Normal file
183
docs/sanic/routing.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# Routing
|
||||
|
||||
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
|
||||
`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
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/tag/<tag>')
|
||||
async def tag_handler(request, 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>')
|
||||
async def integer_handler(request, integer_arg):
|
||||
return text('Integer - {}'.format(integer_arg))
|
||||
|
||||
@app.route('/number/<number_arg:number>')
|
||||
async def number_handler(request, number_arg):
|
||||
return text('Number - {}'.format(number_arg))
|
||||
|
||||
@app.route('/person/<name:[A-z]>')
|
||||
async def person_handler(request, name):
|
||||
return text('Person - {}'.format(name))
|
||||
|
||||
@app.route('/folder/<folder_id:[A-z0-9]{0,4}>')
|
||||
async def folder_handler(request, folder_id):
|
||||
return text('Folder - {}'.format(folder_id))
|
||||
|
||||
```
|
||||
|
||||
## HTTP request types
|
||||
|
||||
By default, a route defined on a URL will be available for only GET requests to that URL.
|
||||
However, the `@app.route` decorator accepts an optional parameter, `methods`,
|
||||
which allows the handler function to work with any of the HTTP methods in the list.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/post', methods=['POST'])
|
||||
async def post_handler(request):
|
||||
return text('POST request - {}'.format(request.json))
|
||||
|
||||
@app.route('/get', methods=['GET'])
|
||||
async def get_handler(request):
|
||||
return text('GET request - {}'.format(request.args))
|
||||
|
||||
```
|
||||
|
||||
There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is a also a route with no host, it will be the default.
|
||||
|
||||
```python
|
||||
@app.route('/get', methods=['GET'], host='example.com')
|
||||
async def get_handler(request):
|
||||
return text('GET request - {}'.format(request.args))
|
||||
|
||||
# if the host header doesn't match example.com, this route will be used
|
||||
@app.route('/get', methods=['GET'])
|
||||
async def get_handler(request):
|
||||
return text('GET request in default - {}'.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.
|
||||
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'])
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
- Multivalue argument can be passed to `url_for`. For example:
|
||||
```python
|
||||
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'])
|
||||
# /posts/5?arg_one=one&arg_one=two
|
||||
```
|
||||
- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not support now and will be ignored). For example:
|
||||
```python
|
||||
url = app.url_for('post_handler', post_id=5, arg_one='one', _anchor='anchor')
|
||||
# /posts/5?arg_one=one#anchor
|
||||
|
||||
url = app.url_for('post_handler', post_id=5, arg_one='one', _external=True)
|
||||
# //server/posts/5?arg_one=one
|
||||
# _external requires passed argument _server or SERVER_NAME in app.config or url will be same as no _external
|
||||
|
||||
url = app.url_for('post_handler', post_id=5, arg_one='one', _scheme='http', _external=True)
|
||||
# http://server/posts/5?arg_one=one
|
||||
# when specifying _scheme, _external must be True
|
||||
|
||||
# you can pass all special arguments one time
|
||||
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2, _anchor='anchor', _scheme='http', _external=True, _server='another_server:8888')
|
||||
# http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor
|
||||
```
|
||||
- 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.
|
||||
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`.
|
||||
73
docs/sanic/testing.md
Normal file
73
docs/sanic/testing.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Testing
|
||||
|
||||
Sanic endpoints can be tested locally using the `test_client` object, which
|
||||
depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
|
||||
library.
|
||||
|
||||
The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods
|
||||
for you to run against your application. A simple example (using pytest) is like follows:
|
||||
|
||||
```python
|
||||
# Import the Sanic app, usually created with Sanic(__name__)
|
||||
from external_server import app
|
||||
|
||||
def test_index_returns_200():
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.status == 200
|
||||
|
||||
def test_index_put_not_allowed():
|
||||
request, response = app.test_client.put('/')
|
||||
assert response.status == 405
|
||||
```
|
||||
|
||||
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.01:42101` and
|
||||
your test request is executed against your application, using `aiohttp`.
|
||||
|
||||
The `test_client` methods accept the following arguments and keyword arguments:
|
||||
|
||||
- `uri` *(default `'/'`)* A string representing the URI to test.
|
||||
- `gather_request` *(default `True`)* A boolean which determines whether the
|
||||
original request will be returned by the function. If set to `True`, the
|
||||
return value is a tuple of `(request, response)`, if `False` only the
|
||||
response is returned.
|
||||
- `server_kwargs` *(default `{}`) a dict of additional arguments to pass into `app.run` before the test request is run.
|
||||
- `debug` *(default `False`)* A boolean which determines whether to run the server in debug mode.
|
||||
|
||||
The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the aiohttp ClientSession request.
|
||||
|
||||
For example, to supply data to a GET request, you would do the following:
|
||||
|
||||
```python
|
||||
def test_get_request_includes_data():
|
||||
params = {'key1': 'value1', 'key2': 'value2'}
|
||||
request, response = app.test_client.get('/', params=params)
|
||||
assert request.args.get('key1') == 'value1'
|
||||
```
|
||||
|
||||
And to supply data to a JSON POST request:
|
||||
|
||||
```python
|
||||
def test_post_json_request_includes_data():
|
||||
data = {'key1': 'value1', 'key2': 'value2'}
|
||||
request, response = app.test_client.post('/', data=json.dumps(data))
|
||||
assert request.json.get('key1') == 'value1'
|
||||
```
|
||||
|
||||
|
||||
More information about
|
||||
the available arguments to aiohttp can be found
|
||||
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
|
||||
|
||||
|
||||
### Deprecated: `sanic_endpoint_test`
|
||||
|
||||
Prior to version 0.3.2, testing was provided through the `sanic_endpoint_test` method. This method will be deprecated in the next major version after 0.4.0; please use the `test_client` instead.
|
||||
|
||||
```
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
def test_index_returns_200():
|
||||
request, response = sanic_endpoint_test(app)
|
||||
assert response.status == 200
|
||||
```
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
# Testing
|
||||
|
||||
Sanic endpoints can be tested locally using the `sanic.utils` module, which
|
||||
depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
|
||||
library. The `sanic_endpoint_test` function runs a local server, issues a
|
||||
configurable request to an endpoint, and returns the result. It takes the
|
||||
following arguments:
|
||||
|
||||
- `app` An instance of a Sanic app.
|
||||
- `method` *(default `'get'`)* A string representing the HTTP method to use.
|
||||
- `uri` *(default `'/'`)* A string representing the endpoint to test.
|
||||
- `gather_request` *(default `True`)* A boolean which determines whether the
|
||||
original request will be returned by the function. If set to `True`, the
|
||||
return value is a tuple of `(request, response)`, if `False` only the
|
||||
response is returned.
|
||||
- `loop` *(default `None`)* The event loop to use.
|
||||
- `debug` *(default `False`)* A boolean which determines whether to run the
|
||||
server in debug mode.
|
||||
|
||||
The function further takes the `*request_args` and `**request_kwargs`, which
|
||||
are passed directly to the aiohttp ClientSession request. For example, to
|
||||
supply data with a GET request, `method` would be `get` and the keyword
|
||||
argument `params={'value', 'key'}` would be supplied. More information about
|
||||
the available arguments to aiohttp can be found
|
||||
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
|
||||
|
||||
Below is a complete example of an endpoint test,
|
||||
using [pytest](http://doc.pytest.org/en/latest/). The test checks that the
|
||||
`/challenge` endpoint responds to a GET request with a supplied challenge
|
||||
string.
|
||||
|
||||
```python
|
||||
import pytest
|
||||
import aiohttp
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
# Import the Sanic app, usually created with Sanic(__name__)
|
||||
from external_server import app
|
||||
|
||||
def test_endpoint_challenge():
|
||||
# Create the challenge data
|
||||
request_data = {'challenge': 'dummy_challenge'}
|
||||
|
||||
# Send the request to the endpoint, using the default `get` method
|
||||
request, response = sanic_endpoint_test(app,
|
||||
uri='/challenge',
|
||||
params=request_data)
|
||||
|
||||
# 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
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Example of caching using aiocache package. To run it you will need a Redis
|
||||
instance running in localhost:6379.
|
||||
instance running in localhost:6379. You can also try with SimpleMemoryCache.
|
||||
|
||||
Running this example you will see that the first call lasts 3 seconds and
|
||||
the rest are instant because the value is retrieved from the Redis.
|
||||
@@ -20,9 +20,14 @@ from aiocache.serializers import JsonSerializer
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
aiocache.settings.set_defaults(
|
||||
class_="aiocache.RedisCache"
|
||||
)
|
||||
|
||||
@app.listener('before_server_start')
|
||||
def init_cache(sanic, loop):
|
||||
aiocache.settings.set_defaults(
|
||||
class_="aiocache.RedisCache",
|
||||
# class_="aiocache.SimpleMemoryCache",
|
||||
loop=loop
|
||||
)
|
||||
|
||||
|
||||
@cached(key="my_custom_key", serializer=JsonSerializer())
|
||||
@@ -38,4 +43,4 @@ async def test(request):
|
||||
return json(await expensive_call())
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, loop=asyncio.get_event_loop())
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
@@ -8,6 +8,7 @@ app = Sanic(__name__)
|
||||
|
||||
sem = None
|
||||
|
||||
@app.listener('before_server_start')
|
||||
def init(sanic, loop):
|
||||
global sem
|
||||
CONCURRENCY_PER_WORKER = 4
|
||||
@@ -33,4 +34,4 @@ async def test(request):
|
||||
return json(response)
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, workers=2, before_start=init)
|
||||
app.run(host="0.0.0.0", port=8000, workers=2)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -26,6 +26,7 @@ async def get_pool():
|
||||
|
||||
app = Sanic(name=__name__)
|
||||
|
||||
@app.listener('before_server_start')
|
||||
async def prepare_db(app, loop):
|
||||
"""
|
||||
Let's create some table and add some data
|
||||
@@ -61,5 +62,4 @@ async def handle(request):
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0',
|
||||
port=8000,
|
||||
debug=True,
|
||||
before_start=prepare_db)
|
||||
debug=True)
|
||||
|
||||
@@ -32,7 +32,7 @@ polls = sa.Table('sanic_polls', metadata,
|
||||
|
||||
app = Sanic(name=__name__)
|
||||
|
||||
|
||||
@app.listener('before_server_start')
|
||||
async def prepare_db(app, loop):
|
||||
""" Let's add some data
|
||||
|
||||
@@ -58,9 +58,10 @@ async def handle(request):
|
||||
async with engine.acquire() as conn:
|
||||
result = []
|
||||
async for row in conn.execute(polls.select()):
|
||||
result.append({"question": row.question, "pub_date": row.pub_date})
|
||||
result.append({"question": row.question,
|
||||
"pub_date": row.pub_date})
|
||||
return json({"polls": result})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000, before_start=prepare_db)
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
|
||||
@@ -27,6 +27,7 @@ def jsonify(records):
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.listener('before_server_start')
|
||||
async def create_db(app, loop):
|
||||
"""
|
||||
Create some table and add some data
|
||||
@@ -55,4 +56,4 @@ async def handler(request):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000, before_start=create_db)
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
|
||||
41
examples/sanic_motor.py
Normal file
41
examples/sanic_motor.py
Normal file
@@ -0,0 +1,41 @@
|
||||
""" sanic motor (async driver for mongodb) example
|
||||
Required packages:
|
||||
pymongo==3.4.0
|
||||
motor==1.1
|
||||
sanic==0.2.0
|
||||
"""
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
app = Sanic('motor_mongodb')
|
||||
|
||||
|
||||
def get_db():
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
mongo_uri = "mongodb://127.0.0.1:27017/test"
|
||||
client = AsyncIOMotorClient(mongo_uri)
|
||||
return client['test']
|
||||
|
||||
|
||||
@app.route('/objects', methods=['GET'])
|
||||
async def get(request):
|
||||
db = get_db()
|
||||
docs = await db.test_col.find().to_list(length=100)
|
||||
for doc in docs:
|
||||
doc['id'] = str(doc['_id'])
|
||||
del doc['_id']
|
||||
return json(docs)
|
||||
|
||||
|
||||
@app.route('/post', methods=['POST'])
|
||||
async def new(request):
|
||||
doc = request.json
|
||||
print(doc)
|
||||
db = get_db()
|
||||
object_id = await db.test_col.save(doc)
|
||||
return json({'object_id': str(object_id)})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host='127.0.0.1', port=8000)
|
||||
@@ -14,14 +14,6 @@ from peewee_async import Manager, PostgresqlDatabase
|
||||
|
||||
# we instantiate a custom loop so we can pass it to our db manager
|
||||
|
||||
def setup(app, loop):
|
||||
database = PostgresqlDatabase(database='test',
|
||||
host='127.0.0.1',
|
||||
user='postgres',
|
||||
password='mysecretpassword')
|
||||
|
||||
objects = Manager(database, loop=loop)
|
||||
|
||||
## from peewee_async docs:
|
||||
# Also there’s no need to connect and re-connect before executing async queries
|
||||
# with manager! It’s all automatic. But you can run Manager.connect() or
|
||||
@@ -48,6 +40,15 @@ objects.database.allow_sync = False # this will raise AssertionError on ANY sync
|
||||
|
||||
app = Sanic('peewee_example')
|
||||
|
||||
@app.listener('before_server_start')
|
||||
def setup(app, loop):
|
||||
database = PostgresqlDatabase(database='test',
|
||||
host='127.0.0.1',
|
||||
user='postgres',
|
||||
password='mysecretpassword')
|
||||
|
||||
objects = Manager(database, loop=loop)
|
||||
|
||||
@app.route('/post/<key>/<value>')
|
||||
async def post(request, key, value):
|
||||
"""
|
||||
@@ -75,4 +76,4 @@ async def get(request):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host='0.0.0.0', port=8000, before_start=setup)
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import os
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.log import log
|
||||
from sanic.response import json, text
|
||||
from sanic.response import json, text, file
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
app = Sanic(__name__)
|
||||
@@ -31,6 +33,10 @@ async def test_await(request):
|
||||
await asyncio.sleep(5)
|
||||
return text("I'm feeling sleepy")
|
||||
|
||||
@app.route("/file")
|
||||
async def test_file(request):
|
||||
return await file(os.path.abspath("setup.py"))
|
||||
|
||||
|
||||
# ----------------------------------------------- #
|
||||
# Exceptions
|
||||
@@ -64,12 +70,14 @@ def query_string(request):
|
||||
# Run Server
|
||||
# ----------------------------------------------- #
|
||||
|
||||
@app.listener('after_server_start')
|
||||
def after_start(app, loop):
|
||||
log.info("OH OH OH OH OHHHHHHHH")
|
||||
|
||||
|
||||
@app.listener('before_server_stop')
|
||||
def before_stop(app, loop):
|
||||
log.info("TRIED EVERYTHING")
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True, after_start=after_start, before_stop=before_stop)
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
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
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
__version__ = '0.3.0'
|
||||
__version__ = '0.4.0'
|
||||
|
||||
__all__ = ['Sanic', 'Blueprint']
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from argparse import ArgumentParser
|
||||
from importlib import import_module
|
||||
|
||||
from .log import log
|
||||
from .sanic import Sanic
|
||||
from sanic.log import log
|
||||
from sanic.app import Sanic
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = ArgumentParser(prog='sanic')
|
||||
|
||||
591
sanic/app.py
Normal file
591
sanic/app.py
Normal file
@@ -0,0 +1,591 @@
|
||||
import logging
|
||||
import re
|
||||
import warnings
|
||||
from asyncio import get_event_loop
|
||||
from collections import deque, defaultdict
|
||||
from functools import partial
|
||||
from inspect import isawaitable, stack, getmodulename
|
||||
from traceback import format_exc
|
||||
from urllib.parse import urlencode, urlunparse
|
||||
|
||||
from sanic.config import Config
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.exceptions import ServerError, URLBuildError, SanicException
|
||||
from sanic.handlers import ErrorHandler
|
||||
from sanic.log import log
|
||||
from sanic.response import HTTPResponse
|
||||
from sanic.router import Router
|
||||
from sanic.server import serve, serve_multiple, HttpProtocol
|
||||
from sanic.static import register as static_register
|
||||
from sanic.testing import TestClient
|
||||
from sanic.views import CompositionView
|
||||
|
||||
|
||||
class Sanic:
|
||||
|
||||
def __init__(self, name=None, router=None, error_handler=None):
|
||||
# Only set up a default log handler if the
|
||||
# end-user application didn't set anything up.
|
||||
if not logging.root.handlers and log.level == logging.NOTSET:
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s: %(levelname)s: %(message)s")
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(formatter)
|
||||
log.addHandler(handler)
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
# Get name from previous stack frame
|
||||
if name is None:
|
||||
frame_records = stack()[1]
|
||||
name = getmodulename(frame_records[1])
|
||||
|
||||
self.name = name
|
||||
self.router = router or Router()
|
||||
self.error_handler = error_handler or ErrorHandler()
|
||||
self.config = Config()
|
||||
self.request_middleware = deque()
|
||||
self.response_middleware = deque()
|
||||
self.blueprints = {}
|
||||
self._blueprint_order = []
|
||||
self.debug = None
|
||||
self.sock = None
|
||||
self.listeners = defaultdict(list)
|
||||
self.is_running = False
|
||||
|
||||
# Register alternative method names
|
||||
self.go_fast = self.run
|
||||
|
||||
@property
|
||||
def loop(self):
|
||||
"""Synonymous with asyncio.get_event_loop().
|
||||
|
||||
Only supported when using the `app.run` method.
|
||||
"""
|
||||
if not self.is_running:
|
||||
raise SanicException(
|
||||
'Loop can only be retrieved after the app has started '
|
||||
'running. Not supported with `create_server` function')
|
||||
return get_event_loop()
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Registration
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
def add_task(self, task):
|
||||
"""Schedule a task to run later, after the loop has started.
|
||||
Different from asyncio.ensure_future in that it does not
|
||||
also return a future, and the actual ensure_future call
|
||||
is delayed until before server start.
|
||||
|
||||
:param task: future, couroutine or awaitable
|
||||
"""
|
||||
@self.listener('before_server_start')
|
||||
def run(app, loop):
|
||||
if callable(task):
|
||||
loop.create_task(task())
|
||||
else:
|
||||
loop.create_task(task)
|
||||
|
||||
# Decorator
|
||||
def listener(self, event):
|
||||
"""Create a listener from a decorated function.
|
||||
|
||||
:param event: event to listen to
|
||||
"""
|
||||
def decorator(listener):
|
||||
self.listeners[event].append(listener)
|
||||
return listener
|
||||
return decorator
|
||||
|
||||
# Decorator
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None):
|
||||
"""Decorate a function to be registered as a route
|
||||
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed
|
||||
:param host:
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
# Fix case where the user did not prefix the URL with a /
|
||||
# and will probably get confused as to why it's not working
|
||||
if not uri.startswith('/'):
|
||||
uri = '/' + uri
|
||||
|
||||
def response(handler):
|
||||
self.router.add(uri=uri, methods=methods, handler=handler,
|
||||
host=host)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
# Shorthand method decorators
|
||||
def get(self, uri, host=None):
|
||||
return self.route(uri, methods=frozenset({"GET"}), host=host)
|
||||
|
||||
def post(self, uri, host=None):
|
||||
return self.route(uri, methods=frozenset({"POST"}), host=host)
|
||||
|
||||
def put(self, uri, host=None):
|
||||
return self.route(uri, methods=frozenset({"PUT"}), host=host)
|
||||
|
||||
def head(self, uri, host=None):
|
||||
return self.route(uri, methods=frozenset({"HEAD"}), host=host)
|
||||
|
||||
def options(self, uri, host=None):
|
||||
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host)
|
||||
|
||||
def patch(self, uri, host=None):
|
||||
return self.route(uri, methods=frozenset({"PATCH"}), host=host)
|
||||
|
||||
def delete(self, uri, host=None):
|
||||
return self.route(uri, methods=frozenset({"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
|
||||
routes.
|
||||
|
||||
:param handler: function or class instance
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed, these are overridden
|
||||
if using a HTTPMethodView
|
||||
:param host:
|
||||
:return: function or class instance
|
||||
"""
|
||||
# Handle HTTPMethodView differently
|
||||
if hasattr(handler, 'view_class'):
|
||||
methods = set()
|
||||
|
||||
for method in HTTP_METHODS:
|
||||
if getattr(handler.view_class, method.lower(), None):
|
||||
methods.add(method)
|
||||
|
||||
# handle composition view differently
|
||||
if isinstance(handler, CompositionView):
|
||||
methods = handler.handlers.keys()
|
||||
|
||||
self.route(uri=uri, methods=methods, host=host)(handler)
|
||||
return handler
|
||||
|
||||
def remove_route(self, uri, clean_cache=True, host=None):
|
||||
self.router.remove(uri, clean_cache, host)
|
||||
|
||||
# Decorator
|
||||
def exception(self, *exceptions):
|
||||
"""Decorate a function to be registered as a handler for exceptions
|
||||
|
||||
:param exceptions: exceptions
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
def response(handler):
|
||||
for exception in exceptions:
|
||||
self.error_handler.add(exception, handler)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
# Decorator
|
||||
def middleware(self, middleware_or_request):
|
||||
"""Decorate and register middleware to be called before a request.
|
||||
Can either be called as @app.middleware or @app.middleware('request')
|
||||
"""
|
||||
def register_middleware(middleware, attach_to='request'):
|
||||
if attach_to == 'request':
|
||||
self.request_middleware.append(middleware)
|
||||
if attach_to == 'response':
|
||||
self.response_middleware.appendleft(middleware)
|
||||
return middleware
|
||||
|
||||
# Detect which way this was called, @middleware or @middleware('AT')
|
||||
if callable(middleware_or_request):
|
||||
return register_middleware(middleware_or_request)
|
||||
|
||||
else:
|
||||
return partial(register_middleware,
|
||||
attach_to=middleware_or_request)
|
||||
|
||||
# Static Files
|
||||
def static(self, uri, file_or_directory, pattern='.+',
|
||||
use_modified_since=True, use_content_range=False):
|
||||
"""Register a root to serve files from. The input can either be a
|
||||
file or a directory. See
|
||||
"""
|
||||
static_register(self, uri, file_or_directory, pattern,
|
||||
use_modified_since, use_content_range)
|
||||
|
||||
def blueprint(self, blueprint, **options):
|
||||
"""Register a blueprint on the application.
|
||||
|
||||
:param blueprint: Blueprint object
|
||||
:param options: option dictionary with blueprint defaults
|
||||
:return: Nothing
|
||||
"""
|
||||
if blueprint.name in self.blueprints:
|
||||
assert self.blueprints[blueprint.name] is blueprint, \
|
||||
'A blueprint with the name "%s" is already registered. ' \
|
||||
'Blueprint names must be unique.' % \
|
||||
(blueprint.name,)
|
||||
else:
|
||||
self.blueprints[blueprint.name] = blueprint
|
||||
self._blueprint_order.append(blueprint)
|
||||
blueprint.register(self, options)
|
||||
|
||||
def register_blueprint(self, *args, **kwargs):
|
||||
# TODO: deprecate 1.0
|
||||
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):
|
||||
"""Build 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: 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))
|
||||
|
||||
if uri != '/' and uri.endswith('/'):
|
||||
uri = uri[:-1]
|
||||
|
||||
out = uri
|
||||
|
||||
# find all the parameters we will need to build in the URL
|
||||
matched_params = re.findall(
|
||||
self.router.parameter_pattern, uri)
|
||||
|
||||
# _method is only a placeholder now, don't know how to support it
|
||||
kwargs.pop('_method', None)
|
||||
anchor = kwargs.pop('_anchor', '')
|
||||
# _external need SERVER_NAME in config or pass _server arg
|
||||
external = kwargs.pop('_external', False)
|
||||
scheme = kwargs.pop('_scheme', '')
|
||||
if scheme and not external:
|
||||
raise ValueError('When specifying _scheme, _external must be True')
|
||||
|
||||
netloc = kwargs.pop('_server', None)
|
||||
if netloc is None and external:
|
||||
netloc = self.config.get('SERVER_NAME', '')
|
||||
|
||||
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
|
||||
query_string = urlencode(kwargs, doseq=True) if kwargs else ''
|
||||
# scheme://netloc/path;parameters?query#fragment
|
||||
out = urlunparse((scheme, netloc, out, '', query_string, anchor))
|
||||
|
||||
return out
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Request Handling
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
def converted_response_type(self, response):
|
||||
pass
|
||||
|
||||
async def handle_request(self, request, response_callback):
|
||||
"""Take a request from the HTTP Server and return a response object
|
||||
to be sent back The HTTP Server only expects a response object, so
|
||||
exception handling must be done here
|
||||
|
||||
:param request: HTTP Request object
|
||||
:param response_callback: Response function to be called with the
|
||||
response as the only argument
|
||||
:return: Nothing
|
||||
"""
|
||||
try:
|
||||
# -------------------------------------------- #
|
||||
# Request Middleware
|
||||
# -------------------------------------------- #
|
||||
|
||||
request.app = self
|
||||
|
||||
response = False
|
||||
# The if improves speed. I don't know why
|
||||
if self.request_middleware:
|
||||
for middleware in self.request_middleware:
|
||||
response = middleware(request)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
if response:
|
||||
break
|
||||
|
||||
# No middleware results
|
||||
if not response:
|
||||
# -------------------------------------------- #
|
||||
# Execute Handler
|
||||
# -------------------------------------------- #
|
||||
|
||||
# Fetch handler from router
|
||||
handler, args, kwargs = self.router.get(request)
|
||||
if handler is None:
|
||||
raise ServerError(
|
||||
("'None' was returned while requesting a "
|
||||
"handler from the router"))
|
||||
|
||||
# Run response handler
|
||||
response = handler(request, *args, **kwargs)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Response Middleware
|
||||
# -------------------------------------------- #
|
||||
|
||||
if self.response_middleware:
|
||||
for middleware in self.response_middleware:
|
||||
_response = middleware(request, response)
|
||||
if isawaitable(_response):
|
||||
_response = await _response
|
||||
if _response:
|
||||
response = _response
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
# -------------------------------------------- #
|
||||
# Response Generation Failed
|
||||
# -------------------------------------------- #
|
||||
|
||||
try:
|
||||
response = self.error_handler.response(request, e)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
except Exception as e:
|
||||
if self.debug:
|
||||
response = HTTPResponse(
|
||||
"Error while handling error: {}\nStack: {}".format(
|
||||
e, format_exc()))
|
||||
else:
|
||||
response = HTTPResponse(
|
||||
"An error occurred while handling an error")
|
||||
|
||||
response_callback(response)
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Testing
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
@property
|
||||
def test_client(self):
|
||||
return TestClient(self)
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Execution
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
def run(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 the HTTP Server and listen until keyboard interrupt or term
|
||||
signal. On termination, drain connections before closing.
|
||||
|
||||
:param host: Address to host on
|
||||
:param port: Port to host on
|
||||
:param debug: Enables debug output (slows server)
|
||||
:param before_start: Functions to be executed before the server starts
|
||||
accepting connections
|
||||
:param after_start: Functions to be executed after the server starts
|
||||
accepting connections
|
||||
:param before_stop: Functions to be executed when a stop signal is
|
||||
received before it is respected
|
||||
:param after_stop: Functions to be executed when all requests are
|
||||
complete
|
||||
:param ssl: SSLContext for SSL encryption of worker(s)
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:param workers: Number of processes
|
||||
received before it is respected
|
||||
:param loop:
|
||||
:param backlog:
|
||||
:param stop_event:
|
||||
:param register_sys_signals:
|
||||
: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:
|
||||
self.is_running = True
|
||||
if workers == 1:
|
||||
serve(**server_settings)
|
||||
else:
|
||||
serve_multiple(server_settings, workers, stop_event)
|
||||
except:
|
||||
log.exception(
|
||||
'Experienced exception while trying to serve')
|
||||
finally:
|
||||
self.is_running = False
|
||||
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`.
|
||||
|
||||
NOTE: This does not support multiprocessing and is not the preferred
|
||||
way to run a Sanic application.
|
||||
"""
|
||||
server_settings = self._helper(
|
||||
host=host, port=port, debug=debug, before_start=before_start,
|
||||
after_start=after_start, before_stop=before_stop,
|
||||
after_stop=after_stop, ssl=ssl, sock=sock,
|
||||
loop=loop or get_event_loop(), protocol=protocol,
|
||||
backlog=backlog, stop_event=stop_event,
|
||||
run_async=True)
|
||||
|
||||
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)
|
||||
|
||||
# Deprecate this
|
||||
if any(arg is not None for arg in (after_stop, after_start,
|
||||
before_start, before_stop)):
|
||||
if debug:
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("Passing a before_start, before_stop, after_start or"
|
||||
"after_stop callback will be deprecated in next "
|
||||
"major version after 0.4.0",
|
||||
DeprecationWarning)
|
||||
|
||||
self.error_handler.debug = debug
|
||||
self.debug = debug
|
||||
|
||||
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,
|
||||
'register_sys_signals': register_sys_signals,
|
||||
'backlog': backlog
|
||||
}
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Register start/stop events
|
||||
# -------------------------------------------- #
|
||||
|
||||
for event_name, settings_name, reverse, args in (
|
||||
("before_server_start", "before_start", False, before_start),
|
||||
("after_server_start", "after_start", False, after_start),
|
||||
("before_server_stop", "before_stop", True, before_stop),
|
||||
("after_server_stop", "after_stop", True, after_stop),
|
||||
):
|
||||
listeners = self.listeners[event_name].copy()
|
||||
if args:
|
||||
if callable(args):
|
||||
listeners.append(args)
|
||||
else:
|
||||
listeners.extend(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
|
||||
|
||||
if debug:
|
||||
log.setLevel(logging.DEBUG)
|
||||
if self.config.LOGO is not None:
|
||||
log.debug(self.config.LOGO)
|
||||
|
||||
if run_async:
|
||||
server_settings['run_async'] = True
|
||||
|
||||
# Serve
|
||||
proto = "http"
|
||||
if ssl is not None:
|
||||
proto = "https"
|
||||
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
|
||||
return server_settings
|
||||
@@ -1,119 +1,114 @@
|
||||
from collections import defaultdict
|
||||
from collections import defaultdict, namedtuple
|
||||
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.views import CompositionView
|
||||
|
||||
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:
|
||||
def __init__(self, name, url_prefix=None, host=None):
|
||||
"""
|
||||
Creates a new blueprint
|
||||
:param name: Unique name of the blueprint
|
||||
"""Create a new blueprint
|
||||
|
||||
:param name: unique name of the blueprint
|
||||
:param url_prefix: URL to be prefixed before all route URLs
|
||||
"""
|
||||
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)
|
||||
"""Register the blueprint to the sanic app."""
|
||||
|
||||
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[1:] if uri.startswith('//') else 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)
|
||||
|
||||
# Event listeners
|
||||
for event, listeners in self.listeners.items():
|
||||
for listener in listeners:
|
||||
app.listener(event)(listener)
|
||||
|
||||
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.
|
||||
"""Create a blueprint route from a decorated function.
|
||||
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param methods: list of acceptable HTTP methods.
|
||||
"""
|
||||
def decorator(handler):
|
||||
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
|
||||
|
||||
def add_route(self, handler, uri, methods=None, host=None):
|
||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None):
|
||||
"""Create a blueprint route from a function.
|
||||
|
||||
:param handler: function for handling uri requests. Accepts function,
|
||||
or class instance with a view_class method.
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param methods: list of acceptable HTTP methods.
|
||||
:return: function or class instance
|
||||
"""
|
||||
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, host))
|
||||
# Handle HTTPMethodView differently
|
||||
if hasattr(handler, 'view_class'):
|
||||
methods = set()
|
||||
|
||||
for method in HTTP_METHODS:
|
||||
if getattr(handler.view_class, method.lower(), None):
|
||||
methods.add(method)
|
||||
|
||||
# handle composition view differently
|
||||
if isinstance(handler, CompositionView):
|
||||
methods = handler.handlers.keys()
|
||||
|
||||
self.route(uri=uri, methods=methods, host=host)(handler)
|
||||
return handler
|
||||
|
||||
def listener(self, event):
|
||||
"""
|
||||
Create a listener from a decorated function.
|
||||
"""Create a listener from a decorated function.
|
||||
|
||||
:param event: Event to listen to.
|
||||
"""
|
||||
def decorator(listener):
|
||||
@@ -122,13 +117,11 @@ class Blueprint:
|
||||
return decorator
|
||||
|
||||
def middleware(self, *args, **kwargs):
|
||||
"""
|
||||
Creates a blueprint middleware from a decorated function.
|
||||
"""
|
||||
def register_middleware(middleware):
|
||||
self.record(
|
||||
lambda s: s.add_middleware(middleware, *args, **kwargs))
|
||||
return middleware
|
||||
"""Create a blueprint middleware from a decorated function."""
|
||||
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]):
|
||||
@@ -139,19 +132,40 @@ class Blueprint:
|
||||
return register_middleware
|
||||
|
||||
def exception(self, *args, **kwargs):
|
||||
"""
|
||||
Creates a blueprint exception from a decorated function.
|
||||
"""
|
||||
"""Create 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
|
||||
|
||||
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.
|
||||
"""Create 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(
|
||||
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,67 @@ 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):
|
||||
"""Load 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):
|
||||
"""Update 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):
|
||||
"""Update 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')
|
||||
@@ -1,4 +1,3 @@
|
||||
from datetime import datetime
|
||||
import re
|
||||
import string
|
||||
|
||||
@@ -39,8 +38,7 @@ _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
|
||||
|
||||
|
||||
class CookieJar(dict):
|
||||
"""
|
||||
CookieJar dynamically writes headers as cookies are added and removed
|
||||
"""CookieJar dynamically writes headers as cookies are added and removed
|
||||
It gets around the limitation of one header per name by using the
|
||||
MultiHeader class to provide a unique key that encodes to Set-Cookie.
|
||||
"""
|
||||
@@ -75,9 +73,7 @@ class CookieJar(dict):
|
||||
|
||||
|
||||
class Cookie(dict):
|
||||
"""
|
||||
This is a stripped down version of Morsel from SimpleCookie #gottagofast
|
||||
"""
|
||||
"""A stripped down version of Morsel from SimpleCookie #gottagofast"""
|
||||
_keys = {
|
||||
"expires": "expires",
|
||||
"path": "Path",
|
||||
@@ -107,13 +103,19 @@ class Cookie(dict):
|
||||
def encode(self, encoding):
|
||||
output = ['%s=%s' % (self.key, _quote(self.value))]
|
||||
for key, value in self.items():
|
||||
if key == 'max-age' and isinstance(value, int):
|
||||
output.append('%s=%d' % (self._keys[key], value))
|
||||
elif key == 'expires' and isinstance(value, datetime):
|
||||
output.append('%s=%s' % (
|
||||
self._keys[key],
|
||||
value.strftime("%a, %d-%b-%Y %T GMT")
|
||||
))
|
||||
if key == 'max-age':
|
||||
try:
|
||||
output.append('%s=%d' % (self._keys[key], value))
|
||||
except TypeError:
|
||||
output.append('%s=%s' % (self._keys[key], value))
|
||||
elif key == 'expires':
|
||||
try:
|
||||
output.append('%s=%s' % (
|
||||
self._keys[key],
|
||||
value.strftime("%a, %d-%b-%Y %T GMT")
|
||||
))
|
||||
except AttributeError:
|
||||
output.append('%s=%s' % (self._keys[key], value))
|
||||
elif key in self._flags:
|
||||
if self[key]:
|
||||
output.append(self._keys[key])
|
||||
@@ -128,9 +130,8 @@ class Cookie(dict):
|
||||
|
||||
|
||||
class MultiHeader:
|
||||
"""
|
||||
Allows us to set a header within response that has a unique key,
|
||||
but may contain duplicate header names
|
||||
"""String-holding object which allow us to set a header within response
|
||||
that has a unique key, but may contain duplicate header names
|
||||
"""
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
from .response import text, html
|
||||
from .log import log
|
||||
from traceback import format_exc, extract_tb
|
||||
import sys
|
||||
|
||||
TRACEBACK_STYLE = '''
|
||||
<style>
|
||||
body {
|
||||
@@ -102,8 +97,10 @@ INTERNAL_SERVER_ERROR_HTML = '''
|
||||
|
||||
|
||||
class SanicException(Exception):
|
||||
|
||||
def __init__(self, message, status_code=None):
|
||||
super().__init__(message)
|
||||
|
||||
if status_code is not None:
|
||||
self.status_code = status_code
|
||||
|
||||
@@ -120,6 +117,10 @@ class ServerError(SanicException):
|
||||
status_code = 500
|
||||
|
||||
|
||||
class URLBuildError(SanicException):
|
||||
status_code = 500
|
||||
|
||||
|
||||
class FileNotFound(NotFound):
|
||||
status_code = 404
|
||||
|
||||
@@ -137,68 +138,20 @@ class PayloadTooLarge(SanicException):
|
||||
status_code = 413
|
||||
|
||||
|
||||
class Handler:
|
||||
handlers = None
|
||||
class HeaderNotFound(SanicException):
|
||||
status_code = 400
|
||||
|
||||
def __init__(self):
|
||||
self.handlers = {}
|
||||
self.debug = False
|
||||
|
||||
def _render_traceback_html(self, exception, request):
|
||||
exc_type, exc_value, tb = sys.exc_info()
|
||||
frames = extract_tb(tb)
|
||||
class ContentRangeError(SanicException):
|
||||
status_code = 416
|
||||
|
||||
frame_html = []
|
||||
for frame in frames:
|
||||
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
|
||||
def __init__(self, message, content_range):
|
||||
super().__init__(message)
|
||||
self.headers = {
|
||||
'Content-Type': 'text/plain',
|
||||
"Content-Range": "bytes */%s" % (content_range.total,)
|
||||
}
|
||||
|
||||
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):
|
||||
self.handlers[exception] = handler
|
||||
|
||||
def response(self, request, exception):
|
||||
"""
|
||||
Fetches and executes an exception handler and returns a response object
|
||||
|
||||
:param request: Request
|
||||
:param exception: Exception to handle
|
||||
:return: Response object
|
||||
"""
|
||||
handler = self.handlers.get(type(exception), self.default)
|
||||
try:
|
||||
response = handler(request=request, exception=exception)
|
||||
except:
|
||||
log.error(format_exc())
|
||||
if self.debug:
|
||||
response_message = (
|
||||
'Exception raised in exception handler "{}" '
|
||||
'for uri: "{}"\n{}').format(
|
||||
handler.__name__, request.url, format_exc())
|
||||
log.error(response_message)
|
||||
return text(response_message, 500)
|
||||
else:
|
||||
return text('An error occurred while handling an error', 500)
|
||||
return response
|
||||
|
||||
def default(self, request, exception):
|
||||
log.error(format_exc())
|
||||
if isinstance(exception, SanicException):
|
||||
return text(
|
||||
'Error: {}'.format(exception),
|
||||
status=getattr(exception, 'status_code', 500))
|
||||
elif self.debug:
|
||||
html_output = self._render_traceback_html(exception, request)
|
||||
|
||||
response_message = (
|
||||
'Exception occurred while handling uri: "{}"\n{}'.format(
|
||||
request.url, format_exc()))
|
||||
log.error(response_message)
|
||||
return html(html_output, status=500)
|
||||
else:
|
||||
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
|
||||
class InvalidRangeType(ContentRangeError):
|
||||
pass
|
||||
|
||||
139
sanic/handlers.py
Normal file
139
sanic/handlers.py
Normal file
@@ -0,0 +1,139 @@
|
||||
import sys
|
||||
from traceback import format_exc, extract_tb
|
||||
|
||||
from sanic.exceptions import (
|
||||
ContentRangeError,
|
||||
HeaderNotFound,
|
||||
INTERNAL_SERVER_ERROR_HTML,
|
||||
InvalidRangeType,
|
||||
SanicException,
|
||||
TRACEBACK_LINE_HTML,
|
||||
TRACEBACK_STYLE,
|
||||
TRACEBACK_WRAPPER_HTML)
|
||||
from sanic.log import log
|
||||
from sanic.response import text, html
|
||||
|
||||
|
||||
class ErrorHandler:
|
||||
handlers = None
|
||||
|
||||
def __init__(self):
|
||||
self.handlers = {}
|
||||
self.debug = False
|
||||
|
||||
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):
|
||||
self.handlers[exception] = handler
|
||||
|
||||
def response(self, request, exception):
|
||||
"""Fetches and executes an exception handler and returns a response
|
||||
object
|
||||
|
||||
:param request: Request
|
||||
:param exception: Exception to handle
|
||||
:return: Response object
|
||||
"""
|
||||
handler = self.handlers.get(type(exception), self.default)
|
||||
try:
|
||||
response = handler(request=request, exception=exception)
|
||||
except Exception:
|
||||
self.log(format_exc())
|
||||
if self.debug:
|
||||
url = getattr(request, 'url', 'unknown')
|
||||
response_message = (
|
||||
'Exception raised in exception handler "{}" '
|
||||
'for uri: "{}"\n{}').format(
|
||||
handler.__name__, url, format_exc())
|
||||
log.error(response_message)
|
||||
return text(response_message, 500)
|
||||
else:
|
||||
return text('An error occurred while handling an error', 500)
|
||||
return response
|
||||
|
||||
def log(self, message, level='error'):
|
||||
"""
|
||||
Override this method in an ErrorHandler subclass to prevent
|
||||
logging exceptions.
|
||||
"""
|
||||
getattr(log, level)(message)
|
||||
|
||||
def default(self, request, exception):
|
||||
self.log(format_exc())
|
||||
if issubclass(type(exception), SanicException):
|
||||
return text(
|
||||
'Error: {}'.format(exception),
|
||||
status=getattr(exception, 'status_code', 500),
|
||||
headers=getattr(exception, 'headers', dict())
|
||||
)
|
||||
elif self.debug:
|
||||
html_output = self._render_traceback_html(exception, request)
|
||||
|
||||
response_message = (
|
||||
'Exception occurred while handling uri: "{}"\n{}'.format(
|
||||
request.url, format_exc()))
|
||||
log.error(response_message)
|
||||
return html(html_output, status=500)
|
||||
else:
|
||||
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
|
||||
|
||||
|
||||
class ContentRangeHandler:
|
||||
"""Class responsible for parsing request header"""
|
||||
__slots__ = ('start', 'end', 'size', 'total', 'headers')
|
||||
|
||||
def __init__(self, request, stats):
|
||||
self.total = stats.st_size
|
||||
_range = request.headers.get('Range')
|
||||
if _range is None:
|
||||
raise HeaderNotFound('Range Header Not Found')
|
||||
unit, _, value = tuple(map(str.strip, _range.partition('=')))
|
||||
if unit != 'bytes':
|
||||
raise InvalidRangeType(
|
||||
'%s is not a valid Range Type' % (unit,), self)
|
||||
start_b, _, end_b = tuple(map(str.strip, value.partition('-')))
|
||||
try:
|
||||
self.start = int(start_b) if start_b else None
|
||||
except ValueError:
|
||||
raise ContentRangeError(
|
||||
'\'%s\' is invalid for Content Range' % (start_b,), self)
|
||||
try:
|
||||
self.end = int(end_b) if end_b else None
|
||||
except ValueError:
|
||||
raise ContentRangeError(
|
||||
'\'%s\' is invalid for Content Range' % (end_b,), self)
|
||||
if self.end is None:
|
||||
if self.start is None:
|
||||
raise ContentRangeError(
|
||||
'Invalid for Content Range parameters', self)
|
||||
else:
|
||||
# this case represents `Content-Range: bytes 5-`
|
||||
self.end = self.total
|
||||
else:
|
||||
if self.start is None:
|
||||
# this case represents `Content-Range: bytes -5`
|
||||
self.start = self.total - self.end
|
||||
self.end = self.total
|
||||
if self.start >= self.end:
|
||||
raise ContentRangeError(
|
||||
'Invalid for Content Range parameters', self)
|
||||
self.size = self.end - self.start
|
||||
self.headers = {
|
||||
'Content-Range': "bytes %s-%s/%s" % (
|
||||
self.start, self.end, self.total)}
|
||||
|
||||
def __bool__(self):
|
||||
return self.size > 0
|
||||
@@ -3,10 +3,14 @@ from collections import namedtuple
|
||||
from http.cookies import SimpleCookie
|
||||
from httptools import parse_url
|
||||
from urllib.parse import parse_qs
|
||||
from ujson import loads as json_loads
|
||||
from sanic.exceptions import InvalidUsage
|
||||
|
||||
from .log import log
|
||||
try:
|
||||
from ujson import loads as json_loads
|
||||
except ImportError:
|
||||
from json import loads as json_loads
|
||||
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.log import log
|
||||
|
||||
|
||||
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||
@@ -16,8 +20,7 @@ DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||
|
||||
|
||||
class RequestParameters(dict):
|
||||
"""
|
||||
Hosts a dict with lists as values where get returns the first
|
||||
"""Hosts a dict with lists as values where get returns the first
|
||||
value of the list and getlist returns the whole shebang
|
||||
"""
|
||||
|
||||
@@ -31,11 +34,9 @@ class RequestParameters(dict):
|
||||
|
||||
|
||||
class Request(dict):
|
||||
"""
|
||||
Properties of an HTTP request such as URL, headers, etc.
|
||||
"""
|
||||
"""Properties of an HTTP request such as URL, headers, etc."""
|
||||
__slots__ = (
|
||||
'url', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||
'app', 'url', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||
'query_string', 'body',
|
||||
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||
'_ip',
|
||||
@@ -44,6 +45,7 @@ class Request(dict):
|
||||
def __init__(self, url_bytes, headers, version, method, transport):
|
||||
# TODO: Content-Encoding detection
|
||||
url_parsed = parse_url(url_bytes)
|
||||
self.app = None
|
||||
self.url = url_parsed.path.decode('utf-8')
|
||||
self.headers = headers
|
||||
self.version = version
|
||||
@@ -73,8 +75,8 @@ class Request(dict):
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
"""
|
||||
Attempts to return the auth header token.
|
||||
"""Attempt to return the auth header token.
|
||||
|
||||
:return: token related to request
|
||||
"""
|
||||
auth_header = self.headers.get('Authorization')
|
||||
@@ -118,8 +120,7 @@ class Request(dict):
|
||||
self.parsed_args = RequestParameters(
|
||||
parse_qs(self.query_string))
|
||||
else:
|
||||
self.parsed_args = {}
|
||||
|
||||
self.parsed_args = RequestParameters()
|
||||
return self.parsed_args
|
||||
|
||||
@property
|
||||
@@ -146,11 +147,10 @@ File = namedtuple('File', ['type', 'body', 'name'])
|
||||
|
||||
|
||||
def parse_multipart_form(body, boundary):
|
||||
"""
|
||||
Parses a request body and returns fields and files
|
||||
"""Parse a request body and returns fields and files
|
||||
|
||||
:param body: Bytes request body
|
||||
:param boundary: Bytes multipart boundary
|
||||
:param body: bytes request body
|
||||
:param boundary: bytes multipart boundary
|
||||
:return: fields (RequestParameters), files (RequestParameters)
|
||||
"""
|
||||
files = RequestParameters()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from aiofiles import open as open_async
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
|
||||
from ujson import dumps as json_dumps
|
||||
|
||||
from .cookies import CookieJar
|
||||
from aiofiles import open as open_async
|
||||
|
||||
from sanic.cookies import CookieJar
|
||||
|
||||
COMMON_STATUS_CODES = {
|
||||
200: b'OK',
|
||||
@@ -98,19 +98,22 @@ class HTTPResponse:
|
||||
# This is all returned in a kind-of funky way
|
||||
# We tried to make this as fast as possible in pure python
|
||||
timeout_header = b''
|
||||
if keep_alive and keep_alive_timeout:
|
||||
timeout_header = b'Keep-Alive: timeout=%d\r\n' % keep_alive_timeout
|
||||
|
||||
if keep_alive and keep_alive_timeout is not None:
|
||||
timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
|
||||
self.headers['Content-Length'] = self.headers.get(
|
||||
'Content-Length', len(self.body))
|
||||
self.headers['Content-Type'] = self.headers.get(
|
||||
'Content-Type', self.content_type)
|
||||
headers = b''
|
||||
if self.headers:
|
||||
for name, value in self.headers.items():
|
||||
try:
|
||||
headers += (
|
||||
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')))
|
||||
except AttributeError:
|
||||
headers += (
|
||||
b'%b: %b\r\n' % (
|
||||
str(name).encode(), str(value).encode('utf-8')))
|
||||
for name, value in self.headers.items():
|
||||
try:
|
||||
headers += (
|
||||
b'%b: %b\r\n' % (
|
||||
name.encode(), value.encode('utf-8')))
|
||||
except AttributeError:
|
||||
headers += (
|
||||
b'%b: %b\r\n' % (
|
||||
str(name).encode(), str(value).encode('utf-8')))
|
||||
|
||||
# Try to pull from the common codes first
|
||||
# Speeds up response rate 6% over pulling from all
|
||||
@@ -119,16 +122,13 @@ class HTTPResponse:
|
||||
status = ALL_STATUS_CODES.get(self.status)
|
||||
|
||||
return (b'HTTP/%b %d %b\r\n'
|
||||
b'Content-Type: %b\r\n'
|
||||
b'Content-Length: %d\r\n'
|
||||
b'Connection: %b\r\n'
|
||||
b'%b%b\r\n'
|
||||
b'%b'
|
||||
b'%b\r\n'
|
||||
b'%b') % (
|
||||
version.encode(),
|
||||
self.status,
|
||||
status,
|
||||
self.content_type.encode(),
|
||||
len(self.body),
|
||||
b'keep-alive' if keep_alive else b'close',
|
||||
timeout_header,
|
||||
headers,
|
||||
@@ -148,21 +148,38 @@ def json(body, status=200, headers=None, **kwargs):
|
||||
:param body: Response data to be serialized.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
:param \**kwargs: Remaining arguments that are passed to the json encoder.
|
||||
:param kwargs: Remaining arguments that are passed to the json encoder.
|
||||
"""
|
||||
return HTTPResponse(json_dumps(body, **kwargs), headers=headers,
|
||||
status=status, content_type="application/json")
|
||||
|
||||
|
||||
def text(body, status=200, headers=None):
|
||||
def text(body, status=200, headers=None,
|
||||
content_type="text/plain; charset=utf-8"):
|
||||
"""
|
||||
Returns response object with body in text format.
|
||||
:param body: Response data to be encoded.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
:param content_type:
|
||||
the content type (string) of the response
|
||||
"""
|
||||
return HTTPResponse(body, status=status, headers=headers,
|
||||
content_type="text/plain; charset=utf-8")
|
||||
content_type=content_type)
|
||||
|
||||
|
||||
def raw(body, status=200, headers=None,
|
||||
content_type="application/octet-stream"):
|
||||
"""
|
||||
Returns response object without encoding the body.
|
||||
:param body: Response data.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
:param content_type:
|
||||
the content type (string) of the response
|
||||
"""
|
||||
return HTTPResponse(body_bytes=body, status=status, headers=headers,
|
||||
content_type=content_type)
|
||||
|
||||
|
||||
def html(body, status=200, headers=None):
|
||||
@@ -176,17 +193,24 @@ def html(body, status=200, headers=None):
|
||||
content_type="text/html; charset=utf-8")
|
||||
|
||||
|
||||
async def file(location, mime_type=None, headers=None):
|
||||
"""
|
||||
Returns response object with file data.
|
||||
async def file(location, mime_type=None, headers=None, _range=None):
|
||||
"""Return a response object with file data.
|
||||
|
||||
:param location: Location of file on system.
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
:param _range:
|
||||
"""
|
||||
filename = path.split(location)[-1]
|
||||
|
||||
async with open_async(location, mode='rb') as _file:
|
||||
out_stream = await _file.read()
|
||||
if _range:
|
||||
await _file.seek(_range.start)
|
||||
out_stream = await _file.read(_range.size)
|
||||
headers['Content-Range'] = 'bytes %s-%s/%s' % (
|
||||
_range.start, _range.end, _range.total)
|
||||
else:
|
||||
out_stream = await _file.read()
|
||||
|
||||
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
|
||||
|
||||
@@ -198,14 +222,12 @@ async def file(location, mime_type=None, headers=None):
|
||||
|
||||
def redirect(to, headers=None, status=302,
|
||||
content_type="text/html; charset=utf-8"):
|
||||
"""
|
||||
Aborts execution and causes a 302 redirect (by default).
|
||||
"""Abort execution and cause 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
|
||||
:param content_type: the content type (string) of the response
|
||||
:returns: the redirecting Response
|
||||
"""
|
||||
headers = headers or {}
|
||||
|
||||
206
sanic/router.py
206
sanic/router.py
@@ -1,11 +1,14 @@
|
||||
import re
|
||||
from collections import defaultdict, namedtuple
|
||||
from collections.abc import Iterable
|
||||
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'])
|
||||
from sanic.exceptions import NotFound, InvalidUsage
|
||||
from sanic.views import CompositionView
|
||||
|
||||
Route = namedtuple(
|
||||
'Route',
|
||||
['handler', 'methods', 'pattern', 'parameters', 'name'])
|
||||
Parameter = namedtuple('Parameter', ['name', 'cast'])
|
||||
|
||||
REGEX_TYPES = {
|
||||
@@ -15,6 +18,8 @@ REGEX_TYPES = {
|
||||
'alpha': (str, r'[A-Za-z]+'),
|
||||
}
|
||||
|
||||
ROUTER_CACHE_SIZE = 1024
|
||||
|
||||
|
||||
def url_hash(url):
|
||||
return url.count('/')
|
||||
@@ -29,8 +34,7 @@ class RouteDoesNotExist(Exception):
|
||||
|
||||
|
||||
class Router:
|
||||
"""
|
||||
Router supports basic routing with parameters and method checks
|
||||
"""Router supports basic routing with parameters and method checks
|
||||
|
||||
Usage:
|
||||
|
||||
@@ -58,42 +62,79 @@ class Router:
|
||||
routes_static = None
|
||||
routes_dynamic = None
|
||||
routes_always_check = None
|
||||
parameter_pattern = re.compile(r'<(.+?)>')
|
||||
|
||||
def __init__(self):
|
||||
self.routes_all = {}
|
||||
self.routes_static = {}
|
||||
self.routes_dynamic = defaultdict(list)
|
||||
self.routes_always_check = []
|
||||
self.hosts = None
|
||||
self.hosts = set()
|
||||
|
||||
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
|
||||
# add regular version
|
||||
self._add(uri, methods, handler, host)
|
||||
slash_is_missing = (not uri[-1].endswith('/')
|
||||
and not self.routes_all.get(uri + '/', False))
|
||||
without_slash_is_missing = (not self.routes_all.get(uri[:-1], False)
|
||||
and uri is not '/')
|
||||
# add version with trailing slash
|
||||
if slash_is_missing:
|
||||
self._add(uri + '/', methods, handler, host)
|
||||
# add version without trailing slash
|
||||
elif without_slash_is_missing:
|
||||
self._add(uri[:-1], methods, handler, host)
|
||||
|
||||
:param uri: Path to match
|
||||
:param methods: Array of accepted method names.
|
||||
If none are provided, any method is allowed
|
||||
:param handler: Request handler function.
|
||||
When executed, it should provide a response object.
|
||||
def _add(self, uri, methods, handler, host=None):
|
||||
"""Add a handler to the route list
|
||||
|
||||
:param uri: path to match
|
||||
:param methods: sequence of accepted method names. If none are
|
||||
provided, any method is allowed
|
||||
:param handler: request handler function.
|
||||
When executed, it should provide a response object.
|
||||
:return: Nothing
|
||||
"""
|
||||
|
||||
if host is not None:
|
||||
# we want to track if there are any
|
||||
# vhosts on the Router instance so that we can
|
||||
# default to the behavior without vhosts
|
||||
if self.hosts is None:
|
||||
self.hosts = set(host)
|
||||
else:
|
||||
if isinstance(host, list):
|
||||
host = set(host)
|
||||
self.hosts.add(host)
|
||||
if isinstance(host, str):
|
||||
uri = host + uri
|
||||
self.hosts.add(host)
|
||||
|
||||
else:
|
||||
for h in host:
|
||||
self.add(uri, methods, handler, h)
|
||||
if not isinstance(host, Iterable):
|
||||
raise ValueError("Expected either string or Iterable of "
|
||||
"host strings, not {!r}".format(host))
|
||||
|
||||
for host_ in host:
|
||||
self.add(uri, methods, handler, host_)
|
||||
return
|
||||
else:
|
||||
# default host
|
||||
self.hosts.add('*')
|
||||
|
||||
# Dict for faster lookups of if method allowed
|
||||
if methods:
|
||||
@@ -103,16 +144,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 +160,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 +185,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 +224,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,55 +251,86 @@ class Router:
|
||||
if clean_cache:
|
||||
self._get.cache_clear()
|
||||
|
||||
def get(self, request):
|
||||
@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)
|
||||
"""
|
||||
Gets a request handler based on the URL of the request, or raises an
|
||||
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):
|
||||
"""Get a request handler based on the URL of the request, or raises an
|
||||
error
|
||||
|
||||
:param request: Request object
|
||||
:return: handler, arguments, keyword arguments
|
||||
"""
|
||||
if self.hosts is None:
|
||||
# No virtual hosts specified; default behavior
|
||||
if not self.hosts:
|
||||
return self._get(request.url, request.method, '')
|
||||
else:
|
||||
# virtual hosts specified; try to match route to the host header
|
||||
try:
|
||||
return self._get(request.url, request.method,
|
||||
request.headers.get("Host", ''))
|
||||
# try default hosts
|
||||
except NotFound:
|
||||
return self._get(request.url, request.method, '')
|
||||
|
||||
@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
|
||||
"""Get a request handler based on the URL of the request, or raises an
|
||||
error. Internal method for caching.
|
||||
:param url: Request URL
|
||||
:param method: Request method
|
||||
|
||||
:param url: request URL
|
||||
:param method: request method
|
||||
:return: handler, arguments, keyword arguments
|
||||
"""
|
||||
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
|
||||
|
||||
427
sanic/sanic.py
427
sanic/sanic.py
@@ -1,427 +0,0 @@
|
||||
import logging
|
||||
from asyncio import get_event_loop
|
||||
from collections import deque
|
||||
from functools import partial
|
||||
from inspect import isawaitable, stack, getmodulename
|
||||
from traceback import format_exc
|
||||
|
||||
from .config import Config
|
||||
from .exceptions import Handler
|
||||
from .exceptions import ServerError
|
||||
from .log import log
|
||||
from .response import HTTPResponse
|
||||
from .router import Router
|
||||
from .server import serve, serve_multiple, HttpProtocol
|
||||
from .static import register as static_register
|
||||
|
||||
|
||||
class Sanic:
|
||||
def __init__(self, name=None, router=None,
|
||||
error_handler=None):
|
||||
# Only set up a default log handler if the
|
||||
# end-user application didn't set anything up.
|
||||
if not logging.root.handlers and log.level == logging.NOTSET:
|
||||
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:
|
||||
frame_records = stack()[1]
|
||||
name = getmodulename(frame_records[1])
|
||||
self.name = name
|
||||
self.router = router or Router()
|
||||
self.error_handler = error_handler or Handler()
|
||||
self.config = Config()
|
||||
self.request_middleware = deque()
|
||||
self.response_middleware = deque()
|
||||
self.blueprints = {}
|
||||
self._blueprint_order = []
|
||||
self.debug = None
|
||||
self.sock = None
|
||||
self.processes = None
|
||||
|
||||
# Register alternative method names
|
||||
self.go_fast = self.run
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Registration
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
# Decorator
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None):
|
||||
"""
|
||||
Decorates a function to be registered as a route
|
||||
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
# Fix case where the user did not prefix the URL with a /
|
||||
# and will probably get confused as to why it's not working
|
||||
if not uri.startswith('/'):
|
||||
uri = '/' + uri
|
||||
|
||||
def response(handler):
|
||||
self.router.add(uri=uri, methods=methods, handler=handler,
|
||||
host=host)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
# 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
|
||||
functions as a handler to the application url
|
||||
routes.
|
||||
|
||||
:param handler: function or class instance
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed
|
||||
:return: function or class instance
|
||||
"""
|
||||
self.route(uri=uri, methods=methods, host=host)(handler)
|
||||
return handler
|
||||
|
||||
def remove_route(self, uri, clean_cache=True, host=None):
|
||||
self.router.remove(uri, clean_cache, host)
|
||||
|
||||
# Decorator
|
||||
def exception(self, *exceptions):
|
||||
"""
|
||||
Decorates a function to be registered as a handler for exceptions
|
||||
|
||||
:param \*exceptions: exceptions
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
def response(handler):
|
||||
for exception in exceptions:
|
||||
self.error_handler.add(exception, handler)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
# Decorator
|
||||
def middleware(self, *args, **kwargs):
|
||||
"""
|
||||
Decorates and registers middleware to be called before a request
|
||||
can either be called as @app.middleware or @app.middleware('request')
|
||||
"""
|
||||
attach_to = 'request'
|
||||
|
||||
def register_middleware(middleware):
|
||||
if attach_to == 'request':
|
||||
self.request_middleware.append(middleware)
|
||||
if attach_to == 'response':
|
||||
self.response_middleware.appendleft(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]):
|
||||
return register_middleware(args[0])
|
||||
else:
|
||||
attach_to = args[0]
|
||||
return register_middleware
|
||||
|
||||
# Static Files
|
||||
def static(self, uri, file_or_directory, pattern='.+',
|
||||
use_modified_since=True):
|
||||
"""
|
||||
Registers a root to serve files from. The input can either be a file
|
||||
or a directory. See
|
||||
"""
|
||||
static_register(self, uri, file_or_directory, pattern,
|
||||
use_modified_since)
|
||||
|
||||
def blueprint(self, blueprint, **options):
|
||||
"""
|
||||
Registers a blueprint on the application.
|
||||
|
||||
:param blueprint: Blueprint object
|
||||
:param options: option dictionary with blueprint defaults
|
||||
:return: Nothing
|
||||
"""
|
||||
if blueprint.name in self.blueprints:
|
||||
assert self.blueprints[blueprint.name] is blueprint, \
|
||||
'A blueprint with the name "%s" is already registered. ' \
|
||||
'Blueprint names must be unique.' % \
|
||||
(blueprint.name,)
|
||||
else:
|
||||
self.blueprints[blueprint.name] = blueprint
|
||||
self._blueprint_order.append(blueprint)
|
||||
blueprint.register(self, options)
|
||||
|
||||
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)
|
||||
return self.blueprint(*args, **kwargs)
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Request Handling
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
def converted_response_type(self, response):
|
||||
pass
|
||||
|
||||
async def handle_request(self, request, response_callback):
|
||||
"""
|
||||
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
|
||||
exception handling must be done here
|
||||
|
||||
:param request: HTTP Request object
|
||||
:param response_callback: Response function to be called with the
|
||||
response as the only argument
|
||||
:return: Nothing
|
||||
"""
|
||||
try:
|
||||
# -------------------------------------------- #
|
||||
# Request Middleware
|
||||
# -------------------------------------------- #
|
||||
|
||||
response = False
|
||||
# The if improves speed. I don't know why
|
||||
if self.request_middleware:
|
||||
for middleware in self.request_middleware:
|
||||
response = middleware(request)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
if response:
|
||||
break
|
||||
|
||||
# No middleware results
|
||||
if not response:
|
||||
# -------------------------------------------- #
|
||||
# Execute Handler
|
||||
# -------------------------------------------- #
|
||||
|
||||
# Fetch handler from router
|
||||
handler, args, kwargs = self.router.get(request)
|
||||
if handler is None:
|
||||
raise ServerError(
|
||||
("'None' was returned while requesting a "
|
||||
"handler from the router"))
|
||||
|
||||
# Run response handler
|
||||
response = handler(request, *args, **kwargs)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Response Middleware
|
||||
# -------------------------------------------- #
|
||||
|
||||
if self.response_middleware:
|
||||
for middleware in self.response_middleware:
|
||||
_response = middleware(request, response)
|
||||
if isawaitable(_response):
|
||||
_response = await _response
|
||||
if _response:
|
||||
response = _response
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
# -------------------------------------------- #
|
||||
# Response Generation Failed
|
||||
# -------------------------------------------- #
|
||||
|
||||
try:
|
||||
response = self.error_handler.response(request, e)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
except Exception as e:
|
||||
if self.debug:
|
||||
response = HTTPResponse(
|
||||
"Error while handling error: {}\nStack: {}".format(
|
||||
e, format_exc()))
|
||||
else:
|
||||
response = HTTPResponse(
|
||||
"An error occurred while handling an error")
|
||||
|
||||
response_callback(response)
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Execution
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
def run(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):
|
||||
"""
|
||||
Runs the HTTP Server and listens until keyboard interrupt or term
|
||||
signal. On termination, drains connections before closing.
|
||||
|
||||
:param host: Address to host on
|
||||
:param port: Port to host on
|
||||
:param debug: Enables debug output (slows server)
|
||||
:param before_start: Functions to be executed before the server starts
|
||||
accepting connections
|
||||
:param after_start: Functions to be executed after the server starts
|
||||
accepting connections
|
||||
:param before_stop: Functions to be executed when a stop signal is
|
||||
received before it is respected
|
||||
:param after_stop: Functions to be executed when all requests are
|
||||
complete
|
||||
:param ssl: SSLContext for SSL encryption of worker(s)
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:param workers: Number of processes
|
||||
received before it is respected
|
||||
:param protocol: Subclass of asyncio protocol class
|
||||
:return: Nothing
|
||||
"""
|
||||
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
|
||||
|
||||
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,
|
||||
'register_sys_signals': register_sys_signals,
|
||||
'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
|
||||
|
||||
if debug:
|
||||
log.setLevel(logging.DEBUG)
|
||||
log.debug(self.config.LOGO)
|
||||
|
||||
# Serve
|
||||
proto = "http"
|
||||
if ssl is not None:
|
||||
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)
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import os
|
||||
import traceback
|
||||
import warnings
|
||||
from functools import partial
|
||||
from inspect import isawaitable
|
||||
from multiprocessing import Process, Event
|
||||
@@ -13,16 +14,15 @@ from time import time
|
||||
from httptools import HttpRequestParser
|
||||
from httptools.parser.errors import HttpParserError
|
||||
|
||||
from .exceptions import ServerError
|
||||
|
||||
try:
|
||||
import uvloop as async_loop
|
||||
except ImportError:
|
||||
async_loop = asyncio
|
||||
|
||||
from .log import log
|
||||
from .request import Request
|
||||
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
|
||||
from sanic.log import log
|
||||
from sanic.request import Request
|
||||
from sanic.exceptions import (
|
||||
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError)
|
||||
|
||||
current_time = None
|
||||
|
||||
@@ -32,8 +32,7 @@ class Signal:
|
||||
|
||||
|
||||
class CIDict(dict):
|
||||
"""
|
||||
Case Insensitive dict where all keys are converted to lowercase
|
||||
"""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
|
||||
"""
|
||||
@@ -168,19 +167,26 @@ class HttpProtocol(asyncio.Protocol):
|
||||
# -------------------------------------------- #
|
||||
|
||||
def write_response(self, response):
|
||||
keep_alive = (
|
||||
self.parser.should_keep_alive() and not self.signal.stopped)
|
||||
try:
|
||||
keep_alive = (
|
||||
self.parser.should_keep_alive() and not self.signal.stopped)
|
||||
self.transport.write(
|
||||
response.output(
|
||||
self.request.version, keep_alive, self.request_timeout))
|
||||
except AttributeError:
|
||||
log.error(
|
||||
('Invalid response object for url {}, '
|
||||
'Expected Type: HTTPResponse, Actual Type: {}').format(
|
||||
self.url, type(response)))
|
||||
self.write_error(ServerError('Invalid response type'))
|
||||
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))
|
||||
"Writing response failed, connection closed {}".format(
|
||||
repr(e)))
|
||||
finally:
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
@@ -197,10 +203,10 @@ class HttpProtocol(asyncio.Protocol):
|
||||
except RuntimeError:
|
||||
log.error(
|
||||
'Connection lost before error written @ {}'.format(
|
||||
self.request.ip))
|
||||
self.request.ip if self.request else 'Unknown'))
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing error failed, connection closed {}".format(e),
|
||||
"Writing error failed, connection closed {}".format(repr(e)),
|
||||
from_error=True)
|
||||
finally:
|
||||
self.transport.close()
|
||||
@@ -227,8 +233,8 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self._total_request_size = 0
|
||||
|
||||
def close_if_idle(self):
|
||||
"""
|
||||
Close the connection if a request is not being sent or received
|
||||
"""Close the connection if a request is not being sent or received
|
||||
|
||||
:return: boolean - True if closed, false if staying open
|
||||
"""
|
||||
if not self.parser:
|
||||
@@ -238,9 +244,8 @@ class HttpProtocol(asyncio.Protocol):
|
||||
|
||||
|
||||
def update_current_time(loop):
|
||||
"""
|
||||
Caches the current time, since it is needed
|
||||
at the end of every keep-alive request to update the request timeout time
|
||||
"""Cache the current time, since it is needed at the end of every
|
||||
keep-alive request to update the request timeout time
|
||||
|
||||
:param loop:
|
||||
:return:
|
||||
@@ -251,17 +256,15 @@ def update_current_time(loop):
|
||||
|
||||
|
||||
def trigger_events(events, loop):
|
||||
"""
|
||||
"""Trigger event callbacks (functions or async)
|
||||
|
||||
:param events: one or more sync or async functions to execute
|
||||
:param loop: event loop
|
||||
"""
|
||||
if events:
|
||||
if not isinstance(events, list):
|
||||
events = [events]
|
||||
for event in events:
|
||||
result = event(loop)
|
||||
if isawaitable(result):
|
||||
loop.run_until_complete(result)
|
||||
for event in events:
|
||||
result = event(loop)
|
||||
if isawaitable(result):
|
||||
loop.run_until_complete(result)
|
||||
|
||||
|
||||
def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
@@ -269,35 +272,35 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
request_timeout=60, ssl=None, sock=None, request_max_size=None,
|
||||
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
|
||||
register_sys_signals=True, run_async=False):
|
||||
"""
|
||||
Starts asynchronous HTTP Server on an individual process.
|
||||
"""Start asynchronous HTTP Server on an individual process.
|
||||
|
||||
:param host: Address to host on
|
||||
:param port: Port to host on
|
||||
:param request_handler: Sanic request handler with middleware
|
||||
:param error_handler: Sanic error handler with middleware
|
||||
:param before_start: Function to be executed before the server starts
|
||||
:param before_start: function to be executed before the server starts
|
||||
listening. Takes arguments `app` instance and `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 arguments `app` instance and `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 arguments
|
||||
`app` instance and `loop`
|
||||
:param after_stop: Function to be executed when a stop signal is
|
||||
:param after_stop: function to be executed when a stop signal is
|
||||
received after it is respected. Takes arguments
|
||||
`app` instance and `loop`
|
||||
:param debug: Enables debug output (slows server)
|
||||
:param debug: enables debug output (slows server)
|
||||
:param request_timeout: time in seconds
|
||||
:param ssl: SSLContext
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:param request_max_size: size in bytes, `None` for no limit
|
||||
:param reuse_port: `True` for multiple workers
|
||||
:param loop: asyncio compatible event loop
|
||||
:param protocol: Subclass of asyncio protocol class
|
||||
: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)
|
||||
@@ -344,8 +347,11 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
# Register signals for graceful termination
|
||||
if register_sys_signals:
|
||||
for _signal in (SIGINT, SIGTERM):
|
||||
loop.add_signal_handler(_signal, loop.stop)
|
||||
|
||||
try:
|
||||
loop.add_signal_handler(_signal, loop.stop)
|
||||
except NotImplementedError:
|
||||
log.warn('Sanic tried to use loop.add_signal_handler but it is'
|
||||
' not implemented on this platform.')
|
||||
pid = os.getpid()
|
||||
try:
|
||||
log.info('Starting worker [{}]'.format(pid))
|
||||
@@ -374,9 +380,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
|
||||
|
||||
def serve_multiple(server_settings, workers, stop_event=None):
|
||||
"""
|
||||
Starts multiple server processes simultaneously. Stops on interrupt
|
||||
and terminate signals, and drains connections when complete.
|
||||
"""Start multiple server processes simultaneously. Stop on interrupt
|
||||
and terminate signals, and drain connections when complete.
|
||||
|
||||
:param server_settings: kw arguments to be passed to the serve function
|
||||
:param workers: number of workers to launch
|
||||
@@ -384,9 +389,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()
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
from aiofiles.os import stat
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
from re import sub
|
||||
from time import strftime, gmtime
|
||||
from urllib.parse import unquote
|
||||
|
||||
from .exceptions import FileNotFound, InvalidUsage
|
||||
from .response import file, HTTPResponse
|
||||
from aiofiles.os import stat
|
||||
|
||||
from sanic.exceptions import (
|
||||
ContentRangeError,
|
||||
FileNotFound,
|
||||
HeaderNotFound,
|
||||
InvalidUsage,
|
||||
)
|
||||
from sanic.handlers import ContentRangeHandler
|
||||
from sanic.response import file, HTTPResponse
|
||||
|
||||
|
||||
def register(app, uri, file_or_directory, pattern, use_modified_since):
|
||||
# TODO: Though sanic is not a file server, I feel like we should atleast
|
||||
def register(app, uri, file_or_directory, pattern,
|
||||
use_modified_since, use_content_range):
|
||||
# TODO: Though sanic is not a file server, I feel like we should at least
|
||||
# make a good effort here. Modified-since is nice, but we could
|
||||
# also look into etags, expires, and caching
|
||||
"""
|
||||
Registers a static directory handler with Sanic by adding a route to the
|
||||
Register a static directory handler with Sanic by adding a route to the
|
||||
router and registering a handler.
|
||||
|
||||
:param app: Sanic
|
||||
@@ -23,8 +32,9 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
|
||||
:param use_modified_since: If true, send file modified time, and return
|
||||
not modified if the browser's matches the
|
||||
server's
|
||||
:param use_content_range: If true, process header for range requests
|
||||
and sends the file part that is requested
|
||||
"""
|
||||
|
||||
# If we're not trying to match a file directly,
|
||||
# serve from the folder
|
||||
if not path.isfile(file_or_directory):
|
||||
@@ -50,18 +60,41 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
|
||||
headers = {}
|
||||
# Check if the client has been sent this file before
|
||||
# and it has not been modified since
|
||||
stats = None
|
||||
if use_modified_since:
|
||||
stats = await stat(file_path)
|
||||
modified_since = strftime('%a, %d %b %Y %H:%M:%S GMT',
|
||||
gmtime(stats.st_mtime))
|
||||
modified_since = strftime(
|
||||
'%a, %d %b %Y %H:%M:%S GMT', gmtime(stats.st_mtime))
|
||||
if request.headers.get('If-Modified-Since') == modified_since:
|
||||
return HTTPResponse(status=304)
|
||||
headers['Last-Modified'] = modified_since
|
||||
|
||||
return await file(file_path, headers=headers)
|
||||
except:
|
||||
_range = None
|
||||
if use_content_range:
|
||||
_range = None
|
||||
if not stats:
|
||||
stats = await stat(file_path)
|
||||
headers['Accept-Ranges'] = 'bytes'
|
||||
headers['Content-Length'] = str(stats.st_size)
|
||||
if request.method != 'HEAD':
|
||||
try:
|
||||
_range = ContentRangeHandler(request, stats)
|
||||
except HeaderNotFound:
|
||||
pass
|
||||
else:
|
||||
del headers['Content-Length']
|
||||
for key, value in _range.headers.items():
|
||||
headers[key] = value
|
||||
if request.method == 'HEAD':
|
||||
return HTTPResponse(
|
||||
headers=headers,
|
||||
content_type=guess_type(file_path)[0] or 'text/plain')
|
||||
else:
|
||||
return await file(file_path, headers=headers, _range=_range)
|
||||
except ContentRangeError:
|
||||
raise
|
||||
except Exception:
|
||||
raise FileNotFound('File not found',
|
||||
path=file_or_directory,
|
||||
relative_url=file_uri)
|
||||
|
||||
app.route(uri, methods=['GET'])(_handler)
|
||||
app.route(uri, methods=['GET', 'HEAD'])(_handler)
|
||||
|
||||
91
sanic/testing.py
Normal file
91
sanic/testing.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from sanic.log import log
|
||||
|
||||
HOST = '127.0.0.1'
|
||||
PORT = 42101
|
||||
|
||||
|
||||
class TestClient:
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
|
||||
import aiohttp
|
||||
if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
|
||||
url = uri
|
||||
else:
|
||||
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.lower())(url, *args, **kwargs) as response:
|
||||
response.text = await response.text()
|
||||
response.body = await response.read()
|
||||
return response
|
||||
|
||||
def _sanic_endpoint_test(
|
||||
self, method='get', uri='/', gather_request=True,
|
||||
debug=False, server_kwargs={},
|
||||
*request_args, **request_kwargs):
|
||||
results = [None, None]
|
||||
exceptions = []
|
||||
|
||||
if gather_request:
|
||||
def _collect_request(request):
|
||||
if results[0] is None:
|
||||
results[0] = request
|
||||
self.app.request_middleware.appendleft(_collect_request)
|
||||
|
||||
@self.app.listener('after_server_start')
|
||||
async def _collect_response(sanic, loop):
|
||||
try:
|
||||
response = await self._local_request(
|
||||
method, uri, *request_args,
|
||||
**request_kwargs)
|
||||
results[-1] = response
|
||||
except Exception as e:
|
||||
exceptions.append(e)
|
||||
self.app.stop()
|
||||
|
||||
self.app.run(host=HOST, debug=debug, port=PORT, **server_kwargs)
|
||||
self.app.listeners['after_server_start'].pop()
|
||||
|
||||
if exceptions:
|
||||
raise ValueError("Exception during request: {}".format(exceptions))
|
||||
|
||||
if gather_request:
|
||||
try:
|
||||
request, response = results
|
||||
return request, response
|
||||
except:
|
||||
raise ValueError(
|
||||
"Request and response object expected, got ({})".format(
|
||||
results))
|
||||
else:
|
||||
try:
|
||||
return results[-1]
|
||||
except:
|
||||
raise ValueError(
|
||||
"Request object expected, got ({})".format(results))
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('get', *args, **kwargs)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('post', *args, **kwargs)
|
||||
|
||||
def put(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('put', *args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('delete', *args, **kwargs)
|
||||
|
||||
def patch(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('patch', *args, **kwargs)
|
||||
|
||||
def options(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('options', *args, **kwargs)
|
||||
|
||||
def head(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('head', *args, **kwargs)
|
||||
@@ -1,57 +1,17 @@
|
||||
import aiohttp
|
||||
from sanic.log import log
|
||||
import warnings
|
||||
|
||||
HOST = '127.0.0.1'
|
||||
PORT = 42101
|
||||
|
||||
|
||||
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:
|
||||
response.text = await response.text()
|
||||
response.body = await response.read()
|
||||
return response
|
||||
from sanic.testing import TestClient
|
||||
|
||||
|
||||
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||
debug=False, server_kwargs={},
|
||||
*request_args, **request_kwargs):
|
||||
results = []
|
||||
exceptions = []
|
||||
warnings.warn(
|
||||
"Use of sanic_endpoint_test will be deprecated in"
|
||||
"the next major version after 0.4.0. Please use the `test_client` "
|
||||
"available on the app object.", DeprecationWarning)
|
||||
|
||||
if gather_request:
|
||||
def _collect_request(request):
|
||||
results.append(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)
|
||||
except Exception as e:
|
||||
exceptions.append(e)
|
||||
app.stop()
|
||||
|
||||
app.run(host=HOST, debug=debug, port=PORT,
|
||||
after_start=_collect_response, **server_kwargs)
|
||||
|
||||
if exceptions:
|
||||
raise ValueError("Exception during request: {}".format(exceptions))
|
||||
|
||||
if gather_request:
|
||||
try:
|
||||
request, response = results
|
||||
return request, response
|
||||
except:
|
||||
raise ValueError(
|
||||
"Request and response object expected, got ({})".format(
|
||||
results))
|
||||
else:
|
||||
try:
|
||||
return results[0]
|
||||
except:
|
||||
raise ValueError(
|
||||
"Request object expected, got ({})".format(results))
|
||||
test_client = TestClient(app)
|
||||
return test_client._sanic_endpoint_test(
|
||||
method, uri, gather_request, debug, server_kwargs,
|
||||
*request_args, **request_kwargs)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from .exceptions import InvalidUsage
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.constants import HTTP_METHODS
|
||||
|
||||
|
||||
class HTTPMethodView:
|
||||
""" Simple class based implementation of view for the sanic.
|
||||
"""Simple class based implementation of view for the sanic.
|
||||
You should implement methods (get, post, put, patch, delete) for the class
|
||||
to every HTTP method you want to support.
|
||||
|
||||
@@ -40,17 +41,12 @@ class HTTPMethodView:
|
||||
|
||||
def dispatch_request(self, request, *args, **kwargs):
|
||||
handler = getattr(self, request.method.lower(), None)
|
||||
if handler:
|
||||
return handler(request, *args, **kwargs)
|
||||
raise InvalidUsage(
|
||||
'Method {} not allowed for URL {}'.format(
|
||||
request.method, request.url), status_code=405)
|
||||
return handler(request, *args, **kwargs)
|
||||
|
||||
@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.
|
||||
|
||||
"""Return view function for use with the routing system, that
|
||||
dispatches request to appropriate handler method.
|
||||
"""
|
||||
def view(*args, **kwargs):
|
||||
self = view.view_class(*class_args, **class_kwargs)
|
||||
@@ -64,11 +60,12 @@ class HTTPMethodView:
|
||||
view.view_class = cls
|
||||
view.__doc__ = cls.__doc__
|
||||
view.__module__ = cls.__module__
|
||||
view.__name__ = cls.__name__
|
||||
return view
|
||||
|
||||
|
||||
class CompositionView:
|
||||
""" Simple method-function mapped view for the sanic.
|
||||
"""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.
|
||||
|
||||
@@ -88,15 +85,15 @@ class CompositionView:
|
||||
|
||||
def add(self, methods, handler):
|
||||
for method in methods:
|
||||
if method not in HTTP_METHODS:
|
||||
raise InvalidUsage(
|
||||
'{} is not a valid HTTP method.'.format(method))
|
||||
|
||||
if method in self.handlers:
|
||||
raise KeyError(
|
||||
'Method {} already is registered.'.format(method))
|
||||
raise InvalidUsage(
|
||||
'Method {} is already 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)
|
||||
handler = self.handlers[request.method.upper()]
|
||||
return handler(request, *args, **kwargs)
|
||||
|
||||
4
setup.py
4
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',
|
||||
@@ -26,7 +26,7 @@ setup(
|
||||
packages=['sanic'],
|
||||
platforms='any',
|
||||
install_requires=[
|
||||
'uvloop>=0.5.3',
|
||||
'uvloop>=0.5.3;platform_system!="Windows"',
|
||||
'httptools>=0.0.9',
|
||||
'ujson>=1.35',
|
||||
'aiofiles>=0.3.0',
|
||||
|
||||
@@ -1 +1 @@
|
||||
I need to be decoded as a uri
|
||||
I am just a regular static file that needs to have its uri decoded
|
||||
|
||||
@@ -5,6 +5,7 @@ from sanic import Sanic
|
||||
def test_bad_request_response():
|
||||
app = Sanic('test_bad_request_response')
|
||||
lines = []
|
||||
@app.listener('after_server_start')
|
||||
async def _request(sanic, loop):
|
||||
connect = asyncio.open_connection('127.0.0.1', 42101)
|
||||
reader, writer = await connect
|
||||
@@ -15,6 +16,6 @@ def test_bad_request_response():
|
||||
break
|
||||
lines.append(line)
|
||||
app.stop()
|
||||
app.run(host='127.0.0.1', port=42101, debug=False, after_start=_request)
|
||||
app.run(host='127.0.0.1', port=42101, debug=False)
|
||||
assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n'
|
||||
assert lines[-1] == b'Error: Bad Request'
|
||||
|
||||
@@ -3,7 +3,6 @@ import inspect
|
||||
from sanic import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.response import json, text
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.exceptions import NotFound, ServerError, InvalidUsage
|
||||
|
||||
|
||||
@@ -20,7 +19,7 @@ def test_bp():
|
||||
return text('Hello')
|
||||
|
||||
app.blueprint(bp)
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.text == 'Hello'
|
||||
|
||||
@@ -33,7 +32,7 @@ def test_bp_with_url_prefix():
|
||||
return text('Hello')
|
||||
|
||||
app.blueprint(bp)
|
||||
request, response = sanic_endpoint_test(app, uri='/test1/')
|
||||
request, response = app.test_client.get('/test1/')
|
||||
|
||||
assert response.text == 'Hello'
|
||||
|
||||
@@ -53,10 +52,10 @@ def test_several_bp_with_url_prefix():
|
||||
|
||||
app.blueprint(bp)
|
||||
app.blueprint(bp2)
|
||||
request, response = sanic_endpoint_test(app, uri='/test1/')
|
||||
request, response = app.test_client.get('/test1/')
|
||||
assert response.text == 'Hello'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test2/')
|
||||
request, response = app.test_client.get('/test2/')
|
||||
assert response.text == 'Hello2'
|
||||
|
||||
def test_bp_with_host():
|
||||
@@ -73,13 +72,15 @@ def test_bp_with_host():
|
||||
|
||||
app.blueprint(bp)
|
||||
headers = {"Host": "example.com"}
|
||||
request, response = sanic_endpoint_test(app, uri='/test1/',
|
||||
headers=headers)
|
||||
request, response = app.test_client.get(
|
||||
'/test1/',
|
||||
headers=headers)
|
||||
assert response.text == 'Hello'
|
||||
|
||||
headers = {"Host": "sub.example.com"}
|
||||
request, response = sanic_endpoint_test(app, uri='/test1/',
|
||||
headers=headers)
|
||||
request, response = app.test_client.get(
|
||||
'/test1/',
|
||||
headers=headers)
|
||||
|
||||
assert response.text == 'Hello subdomain!'
|
||||
|
||||
@@ -111,18 +112,21 @@ def test_several_bp_with_host():
|
||||
|
||||
assert bp.host == "example.com"
|
||||
headers = {"Host": "example.com"}
|
||||
request, response = sanic_endpoint_test(app, uri='/test/',
|
||||
headers=headers)
|
||||
request, response = app.test_client.get(
|
||||
'/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)
|
||||
request, response = app.test_client.get(
|
||||
'/test/',
|
||||
headers=headers)
|
||||
|
||||
assert response.text == 'Hello2'
|
||||
request, response = sanic_endpoint_test(app, uri='/test/other/',
|
||||
headers=headers)
|
||||
request, response = app.test_client.get(
|
||||
'/test/other/',
|
||||
headers=headers)
|
||||
assert response.text == 'Hello3'
|
||||
|
||||
def test_bp_middleware():
|
||||
@@ -139,7 +143,7 @@ def test_bp_middleware():
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
@@ -166,15 +170,15 @@ def test_bp_exception_handler():
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/1')
|
||||
request, response = app.test_client.get('/1')
|
||||
assert response.status == 400
|
||||
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/2')
|
||||
request, response = app.test_client.get('/2')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/3')
|
||||
request, response = app.test_client.get('/3')
|
||||
assert response.status == 200
|
||||
|
||||
def test_bp_listeners():
|
||||
@@ -209,7 +213,7 @@ def test_bp_listeners():
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/')
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert order == [1,2,3,4,5,6]
|
||||
|
||||
@@ -225,6 +229,82 @@ def test_bp_static():
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/testing.file')
|
||||
request, response = app.test_client.get('/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 = app.test_client.get('/get')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = app.test_client.post('/get')
|
||||
assert response.status == 405
|
||||
|
||||
request, response = app.test_client.put('/put')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = app.test_client.get('/post')
|
||||
assert response.status == 405
|
||||
|
||||
request, response = app.test_client.post('/post')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = app.test_client.get('/post')
|
||||
assert response.status == 405
|
||||
|
||||
request, response = app.test_client.head('/head')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = app.test_client.get('/head')
|
||||
assert response.status == 405
|
||||
|
||||
request, response = app.test_client.options('/options')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = app.test_client.get('/options')
|
||||
assert response.status == 405
|
||||
|
||||
request, response = app.test_client.patch('/patch')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = app.test_client.get('/patch')
|
||||
assert response.status == 405
|
||||
|
||||
request, response = app.test_client.delete('/delete')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = app.test_client.get('/delete')
|
||||
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
|
||||
@@ -2,7 +2,6 @@ from datetime import datetime, timedelta
|
||||
from http.cookies import SimpleCookie
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -19,7 +18,7 @@ def test_cookies():
|
||||
response.cookies['right_back'] = 'at you'
|
||||
return response
|
||||
|
||||
request, response = sanic_endpoint_test(app, cookies={"test": "working!"})
|
||||
request, response = app.test_client.get('/', cookies={"test": "working!"})
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
|
||||
@@ -40,7 +39,7 @@ def test_false_cookies(httponly, expected):
|
||||
response.cookies['right_back']['httponly'] = httponly
|
||||
return response
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
|
||||
@@ -55,7 +54,7 @@ def test_http2_cookies():
|
||||
return response
|
||||
|
||||
headers = {'cookie': 'test=working!'}
|
||||
request, response = sanic_endpoint_test(app, headers=headers)
|
||||
request, response = app.test_client.get('/', headers=headers)
|
||||
|
||||
assert response.text == 'Cookies are: working!'
|
||||
|
||||
@@ -70,7 +69,7 @@ def test_cookie_options():
|
||||
response.cookies['test']['expires'] = datetime.now() + timedelta(seconds=10)
|
||||
return response
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
|
||||
@@ -88,7 +87,7 @@ def test_cookie_deletion():
|
||||
del response.cookies['i_never_existed']
|
||||
return response
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
|
||||
|
||||
30
tests/test_create_task.py
Normal file
30
tests/test_create_task.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import sanic
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.response import text
|
||||
from threading import Event
|
||||
import asyncio
|
||||
|
||||
def test_create_task():
|
||||
e = Event()
|
||||
async def coro():
|
||||
await asyncio.sleep(0.05)
|
||||
e.set()
|
||||
|
||||
app = sanic.Sanic()
|
||||
app.add_task(coro)
|
||||
|
||||
@app.route('/early')
|
||||
def not_set(request):
|
||||
return text(e.is_set())
|
||||
|
||||
@app.route('/late')
|
||||
async def set(request):
|
||||
await asyncio.sleep(0.1)
|
||||
return text(e.is_set())
|
||||
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/early')
|
||||
assert response.body == b'False'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/late')
|
||||
assert response.body == b'True'
|
||||
@@ -1,7 +1,6 @@
|
||||
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')
|
||||
|
||||
@@ -26,7 +25,7 @@ def test_use_custom_protocol():
|
||||
server_kwargs = {
|
||||
'protocol': CustomHttpProtocol
|
||||
}
|
||||
request, response = sanic_endpoint_test(app, uri='/1',
|
||||
server_kwargs=server_kwargs)
|
||||
request, response = app.test_client.get(
|
||||
'/1', server_kwargs=server_kwargs)
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
44
tests/test_dynamic_routes.py
Normal file
44
tests/test_dynamic_routes.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
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 = getattr(app.test_client, method)('/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')
|
||||
@@ -4,7 +4,6 @@ from bs4 import BeautifulSoup
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
|
||||
class SanicExceptionTestException(Exception):
|
||||
@@ -48,33 +47,32 @@ def exception_app():
|
||||
|
||||
def test_no_exception(exception_app):
|
||||
"""Test that a route works without an exception"""
|
||||
request, response = sanic_endpoint_test(exception_app)
|
||||
request, response = exception_app.test_client.get('/')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
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 = exception_app.test_client.get('/error')
|
||||
assert response.status == 500
|
||||
|
||||
|
||||
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 = exception_app.test_client.get('/invalid')
|
||||
assert response.status == 400
|
||||
|
||||
|
||||
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 = exception_app.test_client.get('/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')
|
||||
request, response = exception_app.test_client.get('/divide_by_zero')
|
||||
assert response.status == 500
|
||||
soup = BeautifulSoup(response.body, 'html.parser')
|
||||
assert soup.h1.text == 'Internal Server Error'
|
||||
@@ -86,17 +84,16 @@ def test_handled_unhandled_exception(exception_app):
|
||||
|
||||
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')
|
||||
request, response = exception_app.test_client.get(
|
||||
'/error_in_error_handler_handler')
|
||||
assert response.status == 500
|
||||
assert response.body == b'An error occurred while handling an error'
|
||||
|
||||
|
||||
def test_exception_in_exception_handler_debug_off(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',
|
||||
request, response = exception_app.test_client.get(
|
||||
'/error_in_error_handler_handler',
|
||||
debug=False)
|
||||
assert response.status == 500
|
||||
assert response.body == b'An error occurred while handling an error'
|
||||
@@ -104,9 +101,8 @@ def test_exception_in_exception_handler_debug_off(exception_app):
|
||||
|
||||
def test_exception_in_exception_handler_debug_off(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',
|
||||
request, response = exception_app.test_client.get(
|
||||
'/error_in_error_handler_handler',
|
||||
debug=True)
|
||||
assert response.status == 500
|
||||
assert response.body.startswith(b'Exception raised in exception ')
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
exception_handler_app = Sanic('test_exception_handler')
|
||||
@@ -28,37 +27,43 @@ def handler_4(request):
|
||||
return text(foo)
|
||||
|
||||
|
||||
@exception_handler_app.route('/5')
|
||||
def handler_5(request):
|
||||
class CustomServerError(ServerError):
|
||||
status_code=200
|
||||
raise CustomServerError('Custom server error')
|
||||
|
||||
|
||||
@exception_handler_app.exception(NotFound, ServerError)
|
||||
def handler_exception(request, exception):
|
||||
return text("OK")
|
||||
|
||||
|
||||
def test_invalid_usage_exception_handler():
|
||||
request, response = sanic_endpoint_test(exception_handler_app, uri='/1')
|
||||
request, response = exception_handler_app.test_client.get('/1')
|
||||
assert response.status == 400
|
||||
|
||||
|
||||
def test_server_error_exception_handler():
|
||||
request, response = sanic_endpoint_test(exception_handler_app, uri='/2')
|
||||
request, response = exception_handler_app.test_client.get('/2')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
def test_not_found_exception_handler():
|
||||
request, response = sanic_endpoint_test(exception_handler_app, uri='/3')
|
||||
request, response = exception_handler_app.test_client.get('/3')
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_text_exception__handler():
|
||||
request, response = sanic_endpoint_test(
|
||||
exception_handler_app, uri='/random')
|
||||
request, response = exception_handler_app.test_client.get('/random')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
def test_html_traceback_output_in_debug_mode():
|
||||
request, response = sanic_endpoint_test(
|
||||
exception_handler_app, uri='/4', debug=True)
|
||||
request, response = exception_handler_app.test_client.get(
|
||||
'/4', debug=True)
|
||||
assert response.status == 500
|
||||
soup = BeautifulSoup(response.body, 'html.parser')
|
||||
html = str(soup)
|
||||
@@ -71,3 +76,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 = exception_handler_app.test_client.get('/5')
|
||||
assert response.status == 200
|
||||
|
||||
@@ -3,7 +3,6 @@ import uuid
|
||||
from sanic.response import text
|
||||
from sanic import Sanic
|
||||
from io import StringIO
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
import logging
|
||||
|
||||
logging_format = '''module: %(module)s; \
|
||||
@@ -29,7 +28,7 @@ def test_log():
|
||||
log.info(rand_string)
|
||||
return text('hello')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
log_text = log_stream.getvalue()
|
||||
assert rand_string in log_text
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ from json import loads as json_loads, dumps as json_dumps
|
||||
from sanic import Sanic
|
||||
from sanic.request import Request
|
||||
from sanic.response import json, text, HTTPResponse
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -22,7 +21,7 @@ def test_middleware_request():
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.text == 'OK'
|
||||
assert type(results[0]) is Request
|
||||
@@ -46,7 +45,7 @@ def test_middleware_response():
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.text == 'OK'
|
||||
assert type(results[0]) is Request
|
||||
@@ -65,7 +64,7 @@ def test_middleware_override_request():
|
||||
async def handler(request):
|
||||
return text('FAIL')
|
||||
|
||||
response = sanic_endpoint_test(app, gather_request=False)
|
||||
response = app.test_client.get('/', gather_request=False)
|
||||
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
@@ -82,7 +81,7 @@ def test_middleware_override_response():
|
||||
async def handler(request):
|
||||
return text('FAIL')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
@@ -122,7 +121,7 @@ def test_middleware_order():
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.status == 200
|
||||
assert order == [1,2,3,4,5,6]
|
||||
|
||||
@@ -3,7 +3,7 @@ import random
|
||||
import signal
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.utils import HOST, PORT
|
||||
from sanic.testing import HOST, PORT
|
||||
|
||||
|
||||
def test_multiprocessing():
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import PayloadTooLarge
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
data_received_app = Sanic('data_received')
|
||||
data_received_app.config.REQUEST_MAX_SIZE = 1
|
||||
@@ -22,8 +21,7 @@ def handler_exception(request, exception):
|
||||
|
||||
|
||||
def test_payload_too_large_from_error_handler():
|
||||
response = sanic_endpoint_test(
|
||||
data_received_app, uri='/1', gather_request=False)
|
||||
response = data_received_app.test_client.get('/1', gather_request=False)
|
||||
assert response.status == 413
|
||||
assert response.text == 'Payload Too Large from error_handler.'
|
||||
|
||||
@@ -34,8 +32,8 @@ async def handler2(request):
|
||||
|
||||
|
||||
def test_payload_too_large_at_data_received_default():
|
||||
response = sanic_endpoint_test(
|
||||
data_received_default_app, uri='/1', gather_request=False)
|
||||
response = data_received_default_app.test_client.get(
|
||||
'/1', gather_request=False)
|
||||
assert response.status == 413
|
||||
assert response.text == 'Error: Payload Too Large'
|
||||
|
||||
@@ -47,8 +45,7 @@ async def handler3(request):
|
||||
|
||||
def test_payload_too_large_at_on_header_default():
|
||||
data = 'a' * 1000
|
||||
response = sanic_endpoint_test(
|
||||
on_header_default_app, method='post', uri='/1',
|
||||
gather_request=False, data=data)
|
||||
response = on_header_default_app.test_client.post(
|
||||
'/1', gather_request=False, data=data)
|
||||
assert response.status == 413
|
||||
assert response.text == 'Error: Payload Too Large'
|
||||
|
||||
91
tests/test_redirect.py
Normal file
91
tests/test_redirect.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text, redirect
|
||||
|
||||
|
||||
@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 = redirect_app.test_client.get(
|
||||
'/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 = redirect_app.test_client.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 = redirect_app.test_client.get(
|
||||
"/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.
|
||||
"""
|
||||
request, response = redirect_app.test_client.get(
|
||||
"/redirect_init",
|
||||
allow_redirects=True)
|
||||
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
def test_chained_redirect(redirect_app):
|
||||
"""Test test_client is working for redirection"""
|
||||
request, response = redirect_app.test_client.get('/1')
|
||||
assert request.url.endswith('/1')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
assert response.url.endswith('/3')
|
||||
@@ -1,6 +1,7 @@
|
||||
import random
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from ujson import loads
|
||||
|
||||
|
||||
@@ -15,10 +16,28 @@ def test_storage():
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
return json({ 'user': request.get('user'), 'sidekick': request.get('sidekick') })
|
||||
return json({'user': request.get('user'), 'sidekick': request.get('sidekick')})
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
response_json = loads(response.text)
|
||||
assert response_json['user'] == 'sanic'
|
||||
assert response_json.get('sidekick') is None
|
||||
|
||||
|
||||
def test_app_injection():
|
||||
app = Sanic('test_app_injection')
|
||||
expected = random.choice(range(0, 100))
|
||||
|
||||
@app.listener('after_server_start')
|
||||
async def inject_data(app, loop):
|
||||
app.injected = expected
|
||||
|
||||
@app.get('/')
|
||||
async def handler(request):
|
||||
return json({'injected': request.app.injected})
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
response_json = loads(response.text)
|
||||
assert response_json['injected'] == expected
|
||||
|
||||
@@ -2,7 +2,6 @@ from sanic import Sanic
|
||||
import asyncio
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import RequestTimeout
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.config import Config
|
||||
|
||||
Config.REQUEST_TIMEOUT = 1
|
||||
@@ -22,7 +21,7 @@ def handler_exception(request, exception):
|
||||
|
||||
|
||||
def test_server_error_request_timeout():
|
||||
request, response = sanic_endpoint_test(request_timeout_app, uri='/1')
|
||||
request, response = request_timeout_app.test_client.get('/1')
|
||||
assert response.status == 408
|
||||
assert response.text == 'Request Timeout from error_handler.'
|
||||
|
||||
@@ -34,7 +33,6 @@ async def handler_2(request):
|
||||
|
||||
|
||||
def test_default_server_error_request_timeout():
|
||||
request, response = sanic_endpoint_test(
|
||||
request_timeout_default_app, uri='/1')
|
||||
request, response = request_timeout_default_app.test_client.get('/1')
|
||||
assert response.status == 408
|
||||
assert response.text == 'Error: Request Timeout'
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from json import loads as json_loads, dumps as json_dumps
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text, redirect
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.exceptions import ServerError
|
||||
from sanic.response import json, text, redirect
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -17,7 +18,7 @@ def test_sync():
|
||||
def handler(request):
|
||||
return text('Hello')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.text == 'Hello'
|
||||
|
||||
@@ -29,7 +30,7 @@ def test_text():
|
||||
async def handler(request):
|
||||
return text('Hello')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.text == 'Hello'
|
||||
|
||||
@@ -42,7 +43,7 @@ def test_headers():
|
||||
headers = {"spam": "great"}
|
||||
return text('Hello', headers=headers)
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.headers.get('spam') == 'great'
|
||||
|
||||
@@ -55,10 +56,10 @@ def test_non_str_headers():
|
||||
headers = {"answer": 42}
|
||||
return text('Hello', headers=headers)
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.headers.get('answer') == '42'
|
||||
|
||||
|
||||
def test_invalid_response():
|
||||
app = Sanic('test_invalid_response')
|
||||
|
||||
@@ -70,11 +71,11 @@ def test_invalid_response():
|
||||
async def handler(request):
|
||||
return 'This should fail'
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.status == 500
|
||||
assert response.text == "Internal Server Error."
|
||||
|
||||
|
||||
|
||||
|
||||
def test_json():
|
||||
app = Sanic('test_json')
|
||||
|
||||
@@ -82,7 +83,7 @@ def test_json():
|
||||
async def handler(request):
|
||||
return json({"test": True})
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
try:
|
||||
results = json_loads(response.text)
|
||||
@@ -100,7 +101,7 @@ def test_invalid_json():
|
||||
return json(request.json())
|
||||
|
||||
data = "I am not json"
|
||||
request, response = sanic_endpoint_test(app, data=data)
|
||||
request, response = app.test_client.get('/', data=data)
|
||||
|
||||
assert response.status == 400
|
||||
|
||||
@@ -112,7 +113,8 @@ def test_query_string():
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, params=[("test1", "1"), ("test2", "false"), ("test2", "true")])
|
||||
request, response = app.test_client.get(
|
||||
'/', params=[("test1", "1"), ("test2", "false"), ("test2", "true")])
|
||||
|
||||
assert request.args.get('test1') == '1'
|
||||
assert request.args.get('test2') == 'false'
|
||||
@@ -132,7 +134,7 @@ def test_token():
|
||||
'Authorization': 'Token {}'.format(token)
|
||||
}
|
||||
|
||||
request, response = sanic_endpoint_test(app, headers=headers)
|
||||
request, response = app.test_client.get('/', headers=headers)
|
||||
|
||||
assert request.token == token
|
||||
|
||||
@@ -143,14 +145,15 @@ def test_token():
|
||||
def test_post_json():
|
||||
app = Sanic('test_post_json')
|
||||
|
||||
@app.route('/')
|
||||
@app.route('/', methods=['POST'])
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
payload = {'test': 'OK'}
|
||||
headers = {'content-type': 'application/json'}
|
||||
|
||||
request, response = sanic_endpoint_test(app, data=json_dumps(payload), headers=headers)
|
||||
request, response = app.test_client.post(
|
||||
'/', data=json_dumps(payload), headers=headers)
|
||||
|
||||
assert request.json.get('test') == 'OK'
|
||||
assert response.text == 'OK'
|
||||
@@ -159,14 +162,14 @@ def test_post_json():
|
||||
def test_post_form_urlencoded():
|
||||
app = Sanic('test_post_form_urlencoded')
|
||||
|
||||
@app.route('/')
|
||||
@app.route('/', methods=['POST'])
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
payload = 'test=OK'
|
||||
headers = {'content-type': 'application/x-www-form-urlencoded'}
|
||||
|
||||
request, response = sanic_endpoint_test(app, data=payload, headers=headers)
|
||||
request, response = app.test_client.post('/', data=payload, headers=headers)
|
||||
|
||||
assert request.form.get('test') == 'OK'
|
||||
|
||||
@@ -174,7 +177,7 @@ def test_post_form_urlencoded():
|
||||
def test_post_form_multipart_form_data():
|
||||
app = Sanic('test_post_form_multipart_form_data')
|
||||
|
||||
@app.route('/')
|
||||
@app.route('/', methods=['POST'])
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@@ -186,76 +189,6 @@ def test_post_form_multipart_form_data():
|
||||
|
||||
headers = {'content-type': 'multipart/form-data; boundary=----sanic'}
|
||||
|
||||
request, response = sanic_endpoint_test(app, data=payload, headers=headers)
|
||||
request, response = app.test_client.post(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'
|
||||
|
||||
@@ -2,7 +2,6 @@ from random import choice
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import HTTPResponse
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
|
||||
def test_response_body_not_a_string():
|
||||
@@ -14,5 +13,5 @@ def test_response_body_not_a_string():
|
||||
async def hello_route(request):
|
||||
return HTTPResponse(body=random_num)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/hello')
|
||||
request, response = app.test_client.get('/hello')
|
||||
assert response.text == str(random_num)
|
||||
|
||||
@@ -3,7 +3,6 @@ import pytest
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.router import RouteExists, RouteDoesNotExist
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -17,12 +16,25 @@ def test_shorthand_routes_get():
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/get', method='get')
|
||||
request, response = app.test_client.get('/get')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/get', method='post')
|
||||
request, response = app.test_client.post('/get')
|
||||
assert response.status == 405
|
||||
|
||||
def test_route_optional_slash():
|
||||
app = Sanic('test_route_optional_slash')
|
||||
|
||||
@app.get('/get')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/get')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = app.test_client.get('/get/')
|
||||
assert response.text == 'OK'
|
||||
|
||||
def test_shorthand_routes_post():
|
||||
app = Sanic('test_shorhand_routes_post')
|
||||
|
||||
@@ -30,10 +42,10 @@ def test_shorthand_routes_post():
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/post', method='post')
|
||||
request, response = app.test_client.post('/post')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/post', method='get')
|
||||
request, response = app.test_client.get('/post')
|
||||
assert response.status == 405
|
||||
|
||||
def test_shorthand_routes_put():
|
||||
@@ -43,10 +55,10 @@ def test_shorthand_routes_put():
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/put', method='put')
|
||||
request, response = app.test_client.put('/put')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/put', method='get')
|
||||
request, response = app.test_client.get('/put')
|
||||
assert response.status == 405
|
||||
|
||||
def test_shorthand_routes_patch():
|
||||
@@ -56,10 +68,10 @@ def test_shorthand_routes_patch():
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/patch', method='patch')
|
||||
request, response = app.test_client.patch('/patch')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/patch', method='get')
|
||||
request, response = app.test_client.get('/patch')
|
||||
assert response.status == 405
|
||||
|
||||
def test_shorthand_routes_head():
|
||||
@@ -69,10 +81,10 @@ def test_shorthand_routes_head():
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/head', method='head')
|
||||
request, response = app.test_client.head('/head')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/head', method='get')
|
||||
request, response = app.test_client.get('/head')
|
||||
assert response.status == 405
|
||||
|
||||
def test_shorthand_routes_options():
|
||||
@@ -82,10 +94,10 @@ def test_shorthand_routes_options():
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/options', method='options')
|
||||
request, response = app.test_client.options('/options')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/options', method='get')
|
||||
request, response = app.test_client.get('/options')
|
||||
assert response.status == 405
|
||||
|
||||
def test_static_routes():
|
||||
@@ -99,10 +111,10 @@ def test_static_routes():
|
||||
async def handler2(request):
|
||||
return text('OK2')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test')
|
||||
request, response = app.test_client.get('/test')
|
||||
assert response.text == 'OK1'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/pizazz')
|
||||
request, response = app.test_client.get('/pizazz')
|
||||
assert response.text == 'OK2'
|
||||
|
||||
|
||||
@@ -116,7 +128,7 @@ def test_dynamic_route():
|
||||
results.append(name)
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||
request, response = app.test_client.get('/folder/test123')
|
||||
|
||||
assert response.text == 'OK'
|
||||
assert results[0] == 'test123'
|
||||
@@ -132,12 +144,12 @@ def test_dynamic_route_string():
|
||||
results.append(name)
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||
request, response = app.test_client.get('/folder/test123')
|
||||
|
||||
assert response.text == 'OK'
|
||||
assert results[0] == 'test123'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico')
|
||||
request, response = app.test_client.get('/folder/favicon.ico')
|
||||
|
||||
assert response.text == 'OK'
|
||||
assert results[1] == 'favicon.ico'
|
||||
@@ -153,11 +165,11 @@ def test_dynamic_route_int():
|
||||
results.append(folder_id)
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/12345')
|
||||
request, response = app.test_client.get('/folder/12345')
|
||||
assert response.text == 'OK'
|
||||
assert type(results[0]) is int
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/asdf')
|
||||
request, response = app.test_client.get('/folder/asdf')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
@@ -171,14 +183,14 @@ def test_dynamic_route_number():
|
||||
results.append(weight)
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/weight/12345')
|
||||
request, response = app.test_client.get('/weight/12345')
|
||||
assert response.text == 'OK'
|
||||
assert type(results[0]) is float
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/weight/1234.56')
|
||||
request, response = app.test_client.get('/weight/1234.56')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/weight/1234-56')
|
||||
request, response = app.test_client.get('/weight/1234-56')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
@@ -189,16 +201,16 @@ def test_dynamic_route_regex():
|
||||
async def handler(request, folder_id):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test')
|
||||
request, response = app.test_client.get('/folder/test')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test1')
|
||||
request, response = app.test_client.get('/folder/test1')
|
||||
assert response.status == 404
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test-123')
|
||||
request, response = app.test_client.get('/folder/test-123')
|
||||
assert response.status == 404
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/')
|
||||
request, response = app.test_client.get('/folder/')
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
@@ -209,16 +221,16 @@ def test_dynamic_route_unhashable():
|
||||
async def handler(request, unhashable):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
|
||||
request, response = app.test_client.get('/folder/test/asdf/end/')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
|
||||
request, response = app.test_client.get('/folder/test///////end/')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
|
||||
request, response = app.test_client.get('/folder/test/end/')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test/nope/')
|
||||
request, response = app.test_client.get('/folder/test/nope/')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
@@ -251,10 +263,10 @@ def test_method_not_allowed():
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test')
|
||||
request, response = app.test_client.get('/test')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, method='post', uri='/test')
|
||||
request, response = app.test_client.post('/test')
|
||||
assert response.status == 405
|
||||
|
||||
|
||||
@@ -270,10 +282,10 @@ def test_static_add_route():
|
||||
app.add_route(handler1, '/test')
|
||||
app.add_route(handler2, '/test2')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test')
|
||||
request, response = app.test_client.get('/test')
|
||||
assert response.text == 'OK1'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test2')
|
||||
request, response = app.test_client.get('/test2')
|
||||
assert response.text == 'OK2'
|
||||
|
||||
|
||||
@@ -287,7 +299,7 @@ def test_dynamic_add_route():
|
||||
return text('OK')
|
||||
|
||||
app.add_route(handler, '/folder/<name>')
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||
request, response = app.test_client.get('/folder/test123')
|
||||
|
||||
assert response.text == 'OK'
|
||||
assert results[0] == 'test123'
|
||||
@@ -303,12 +315,12 @@ def test_dynamic_add_route_string():
|
||||
return text('OK')
|
||||
|
||||
app.add_route(handler, '/folder/<name:string>')
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||
request, response = app.test_client.get('/folder/test123')
|
||||
|
||||
assert response.text == 'OK'
|
||||
assert results[0] == 'test123'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico')
|
||||
request, response = app.test_client.get('/folder/favicon.ico')
|
||||
|
||||
assert response.text == 'OK'
|
||||
assert results[1] == 'favicon.ico'
|
||||
@@ -325,11 +337,11 @@ def test_dynamic_add_route_int():
|
||||
|
||||
app.add_route(handler, '/folder/<folder_id:int>')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/12345')
|
||||
request, response = app.test_client.get('/folder/12345')
|
||||
assert response.text == 'OK'
|
||||
assert type(results[0]) is int
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/asdf')
|
||||
request, response = app.test_client.get('/folder/asdf')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
@@ -344,14 +356,14 @@ def test_dynamic_add_route_number():
|
||||
|
||||
app.add_route(handler, '/weight/<weight:number>')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/weight/12345')
|
||||
request, response = app.test_client.get('/weight/12345')
|
||||
assert response.text == 'OK'
|
||||
assert type(results[0]) is float
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/weight/1234.56')
|
||||
request, response = app.test_client.get('/weight/1234.56')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/weight/1234-56')
|
||||
request, response = app.test_client.get('/weight/1234-56')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
@@ -363,16 +375,16 @@ def test_dynamic_add_route_regex():
|
||||
|
||||
app.add_route(handler, '/folder/<folder_id:[A-Za-z0-9]{0,4}>')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test')
|
||||
request, response = app.test_client.get('/folder/test')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test1')
|
||||
request, response = app.test_client.get('/folder/test1')
|
||||
assert response.status == 404
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test-123')
|
||||
request, response = app.test_client.get('/folder/test-123')
|
||||
assert response.status == 404
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/')
|
||||
request, response = app.test_client.get('/folder/')
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
@@ -384,16 +396,16 @@ def test_dynamic_add_route_unhashable():
|
||||
|
||||
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
|
||||
request, response = app.test_client.get('/folder/test/asdf/end/')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
|
||||
request, response = app.test_client.get('/folder/test///////end/')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
|
||||
request, response = app.test_client.get('/folder/test/end/')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test/nope/')
|
||||
request, response = app.test_client.get('/folder/test/nope/')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
@@ -429,10 +441,10 @@ def test_add_route_method_not_allowed():
|
||||
|
||||
app.add_route(handler, '/test', methods=['GET'])
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test')
|
||||
request, response = app.test_client.get('/test')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, method='post', uri='/test')
|
||||
request, response = app.test_client.post('/test')
|
||||
assert response.status == 405
|
||||
|
||||
|
||||
@@ -448,19 +460,19 @@ def test_remove_static_route():
|
||||
app.add_route(handler1, '/test')
|
||||
app.add_route(handler2, '/test2')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test')
|
||||
request, response = app.test_client.get('/test')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test2')
|
||||
request, response = app.test_client.get('/test2')
|
||||
assert response.status == 200
|
||||
|
||||
app.remove_route('/test')
|
||||
app.remove_route('/test2')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test')
|
||||
request, response = app.test_client.get('/test')
|
||||
assert response.status == 404
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test2')
|
||||
request, response = app.test_client.get('/test2')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
@@ -472,11 +484,11 @@ def test_remove_dynamic_route():
|
||||
|
||||
app.add_route(handler, '/folder/<name>')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||
request, response = app.test_client.get('/folder/test123')
|
||||
assert response.status == 200
|
||||
|
||||
app.remove_route('/folder/<name>')
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||
request, response = app.test_client.get('/folder/test123')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
@@ -495,24 +507,24 @@ def test_remove_unhashable_route():
|
||||
|
||||
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
|
||||
request, response = app.test_client.get('/folder/test/asdf/end/')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
|
||||
request, response = app.test_client.get('/folder/test///////end/')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
|
||||
request, response = app.test_client.get('/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/')
|
||||
request, response = app.test_client.get('/folder/test/asdf/end/')
|
||||
assert response.status == 404
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
|
||||
request, response = app.test_client.get('/folder/test///////end/')
|
||||
assert response.status == 404
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
|
||||
request, response = app.test_client.get('/folder/test/end/')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
@@ -524,22 +536,22 @@ def test_remove_route_without_clean_cache():
|
||||
|
||||
app.add_route(handler, '/test')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test')
|
||||
request, response = app.test_client.get('/test')
|
||||
assert response.status == 200
|
||||
|
||||
app.remove_route('/test', clean_cache=True)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test')
|
||||
request, response = app.test_client.get('/test')
|
||||
assert response.status == 404
|
||||
|
||||
app.add_route(handler, '/test')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test')
|
||||
request, response = app.test_client.get('/test')
|
||||
assert response.status == 200
|
||||
|
||||
app.remove_route('/test', clean_cache=False)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test')
|
||||
request, response = app.test_client.get('/test')
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
@@ -554,16 +566,16 @@ def test_overload_routes():
|
||||
async def handler2(request):
|
||||
return text('OK2')
|
||||
|
||||
request, response = sanic_endpoint_test(app, 'get', uri='/overload')
|
||||
request, response = app.test_client.get('/overload')
|
||||
assert response.text == 'OK1'
|
||||
|
||||
request, response = sanic_endpoint_test(app, 'post', uri='/overload')
|
||||
request, response = app.test_client.post('/overload')
|
||||
assert response.text == 'OK2'
|
||||
|
||||
request, response = sanic_endpoint_test(app, 'put', uri='/overload')
|
||||
request, response = app.test_client.put('/overload')
|
||||
assert response.text == 'OK2'
|
||||
|
||||
request, response = sanic_endpoint_test(app, 'delete', uri='/overload')
|
||||
request, response = app.test_client.delete('/overload')
|
||||
assert response.status == 405
|
||||
|
||||
with pytest.raises(RouteExists):
|
||||
@@ -584,10 +596,10 @@ def test_unmergeable_overload_routes():
|
||||
async def handler2(request):
|
||||
return text('Duplicated')
|
||||
|
||||
request, response = sanic_endpoint_test(app, 'get', uri='/overload_whole')
|
||||
request, response = app.test_client.get('/overload_whole')
|
||||
assert response.text == 'OK1'
|
||||
|
||||
request, response = sanic_endpoint_test(app, 'post', uri='/overload_whole')
|
||||
request, response = app.test_client.post('/overload_whole')
|
||||
assert response.text == 'OK1'
|
||||
|
||||
|
||||
@@ -600,8 +612,8 @@ def test_unmergeable_overload_routes():
|
||||
async def handler2(request):
|
||||
return text('Duplicated')
|
||||
|
||||
request, response = sanic_endpoint_test(app, 'get', uri='/overload_part')
|
||||
request, response = app.test_client.get('/overload_part')
|
||||
assert response.text == 'OK1'
|
||||
|
||||
request, response = sanic_endpoint_test(app, 'post', uri='/overload_part')
|
||||
request, response = app.test_client.post('/overload_part')
|
||||
assert response.status == 405
|
||||
|
||||
@@ -6,12 +6,13 @@ import signal
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.testing import HOST, PORT
|
||||
|
||||
AVAILABLE_LISTENERS = [
|
||||
'before_start',
|
||||
'after_start',
|
||||
'before_stop',
|
||||
'after_stop'
|
||||
'before_server_start',
|
||||
'after_server_start',
|
||||
'before_server_stop',
|
||||
'after_server_stop'
|
||||
]
|
||||
|
||||
|
||||
@@ -30,7 +31,7 @@ def start_stop_app(random_name_app, **run_kwargs):
|
||||
signal.signal(signal.SIGALRM, stop_on_alarm)
|
||||
signal.alarm(1)
|
||||
try:
|
||||
random_name_app.run(**run_kwargs)
|
||||
random_name_app.run(HOST, PORT, **run_kwargs)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
@@ -41,9 +42,10 @@ def test_single_listener(listener_name):
|
||||
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)})
|
||||
# Register listener
|
||||
random_name_app.listener(listener_name)(
|
||||
create_listener(listener_name, output))
|
||||
start_stop_app(random_name_app)
|
||||
assert random_name_app.name + listener_name == output.pop()
|
||||
|
||||
|
||||
@@ -51,9 +53,9 @@ 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:
|
||||
listener = create_listener(listener_name, output)
|
||||
random_name_app.listener(listener_name)(listener)
|
||||
start_stop_app(random_name_app)
|
||||
for listener_name in AVAILABLE_LISTENERS:
|
||||
assert random_name_app.name + listener_name == output.pop()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import HTTPResponse
|
||||
from sanic.utils import HOST, PORT
|
||||
from sanic.testing import HOST, PORT
|
||||
from unittest.mock import MagicMock
|
||||
import pytest
|
||||
import asyncio
|
||||
@@ -27,10 +27,11 @@ def test_register_system_signals():
|
||||
async def hello_route(request):
|
||||
return HTTPResponse()
|
||||
|
||||
app.run(HOST, PORT,
|
||||
before_start=set_loop,
|
||||
after_start=stop,
|
||||
after_stop=after)
|
||||
app.listener('after_server_start')(stop)
|
||||
app.listener('before_server_start')(set_loop)
|
||||
app.listener('after_server_stop')(after)
|
||||
|
||||
app.run(HOST, PORT)
|
||||
assert calledq.get() == True
|
||||
|
||||
|
||||
@@ -42,9 +43,9 @@ def test_dont_register_system_signals():
|
||||
async def hello_route(request):
|
||||
return HTTPResponse()
|
||||
|
||||
app.run(HOST, PORT,
|
||||
before_start=set_loop,
|
||||
after_start=stop,
|
||||
after_stop=after,
|
||||
register_sys_signals=False)
|
||||
app.listener('after_server_start')(stop)
|
||||
app.listener('before_server_start')(set_loop)
|
||||
app.listener('after_server_stop')(after)
|
||||
|
||||
app.run(HOST, PORT, register_sys_signals=False)
|
||||
assert calledq.get() == False
|
||||
|
||||
@@ -4,7 +4,6 @@ import os
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
@@ -16,47 +15,149 @@ 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')
|
||||
request, response = app.test_client.get('/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 = app.test_client.get(
|
||||
uri='{}/{}'.format(base_uri, file_name))
|
||||
assert response.status == 200
|
||||
assert response.body == static_file_content
|
||||
assert response.body == get_file_content(static_file_directory, file_name)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_head_request(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
app.static('/dir', static_file_directory)
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/dir/decode me.txt')
|
||||
request, response = app.test_client.head('/testing.file')
|
||||
assert response.status == 200
|
||||
assert response.body == decode_me_contents
|
||||
assert 'Accept-Ranges' in response.headers
|
||||
assert 'Content-Length' in response.headers
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(
|
||||
get_file_content(static_file_directory, file_name))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_correct(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
headers = {
|
||||
'Range': 'bytes=12-19'
|
||||
}
|
||||
request, response = app.test_client.get('/testing.file', headers=headers)
|
||||
assert response.status == 200
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
static_file_directory, file_name))[12:19]
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(static_content)
|
||||
assert response.body == static_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_front(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
headers = {
|
||||
'Range': 'bytes=12-'
|
||||
}
|
||||
request, response = app.test_client.get('/testing.file', headers=headers)
|
||||
assert response.status == 200
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
static_file_directory, file_name))[12:]
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(static_content)
|
||||
assert response.body == static_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_back(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
headers = {
|
||||
'Range': 'bytes=-12'
|
||||
}
|
||||
request, response = app.test_client.get('/testing.file', headers=headers)
|
||||
assert response.status == 200
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
static_file_directory, file_name))[-12:]
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(static_content)
|
||||
assert response.body == static_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_empty(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
request, response = app.test_client.get('/testing.file')
|
||||
assert response.status == 200
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' not in response.headers
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(get_file_content(static_file_directory, file_name))
|
||||
assert response.body == bytes(
|
||||
get_file_content(static_file_directory, file_name))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_error(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
headers = {
|
||||
'Range': 'bytes=1-0'
|
||||
}
|
||||
request, response = app.test_client.get('/testing.file', headers=headers)
|
||||
assert response.status == 416
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
assert response.headers['Content-Range'] == "bytes */%s" % (
|
||||
len(get_file_content(static_file_directory, file_name)),)
|
||||
|
||||
298
tests/test_url_building.py
Normal file
298
tests/test_url_building.py
Normal file
@@ -0,0 +1,298 @@
|
||||
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.testing import PORT as test_port
|
||||
from sanic.exceptions import URLBuildError
|
||||
|
||||
import string
|
||||
|
||||
URL_FOR_ARGS1 = dict(arg1=['v1', 'v2'])
|
||||
URL_FOR_VALUE1 = '/myurl?arg1=v1&arg1=v2'
|
||||
URL_FOR_ARGS2 = dict(arg1=['v1', 'v2'], _anchor='anchor')
|
||||
URL_FOR_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor'
|
||||
URL_FOR_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http',
|
||||
_server='localhost:{}'.format(test_port), _external=True)
|
||||
URL_FOR_VALUE3 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(test_port)
|
||||
|
||||
|
||||
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 = simple_app.test_client.get(url)
|
||||
assert response.status == 200
|
||||
assert response.text == letter
|
||||
|
||||
|
||||
@pytest.mark.parametrize('args,url',
|
||||
[(URL_FOR_ARGS1, URL_FOR_VALUE1),
|
||||
(URL_FOR_ARGS2, URL_FOR_VALUE2),
|
||||
(URL_FOR_ARGS3, URL_FOR_VALUE3)])
|
||||
def test_simple_url_for_getting_with_more_params(args, url):
|
||||
app = Sanic('more_url_build')
|
||||
|
||||
@app.route('/myurl')
|
||||
def passes(request):
|
||||
return text('this should pass')
|
||||
|
||||
assert url == app.url_for('passes', **args)
|
||||
request, response = app.test_client.get(url)
|
||||
assert response.status == 200
|
||||
assert response.text == 'this should pass'
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def test_fails_url_build_if_params_not_passed():
|
||||
app = Sanic('fail_url_build')
|
||||
|
||||
@app.route('/fail')
|
||||
def fail():
|
||||
return text('this should fail')
|
||||
|
||||
with pytest.raises(ValueError) as e:
|
||||
app.url_for('fail', _scheme='http')
|
||||
|
||||
assert str(e.value) == 'When specifying _scheme, _external must be True'
|
||||
|
||||
|
||||
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,7 +1,6 @@
|
||||
from json import loads as json_loads, dumps as json_dumps
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -15,7 +14,7 @@ def test_utf8_query_string():
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, params=[("utf8", '✓')])
|
||||
request, response = app.test_client.get('/', params=[("utf8", '✓')])
|
||||
assert request.args.get('utf8') == '✓'
|
||||
|
||||
|
||||
@@ -26,7 +25,7 @@ def test_utf8_response():
|
||||
async def handler(request):
|
||||
return text('✓')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.text == '✓'
|
||||
|
||||
|
||||
@@ -38,7 +37,7 @@ def skip_test_utf8_route():
|
||||
return text('OK')
|
||||
|
||||
# UTF-8 Paths are not supported
|
||||
request, response = sanic_endpoint_test(app, route='/✓', uri='/✓')
|
||||
request, response = app.test_client.get('/✓')
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
@@ -52,7 +51,9 @@ def test_utf8_post_json():
|
||||
payload = {'test': '✓'}
|
||||
headers = {'content-type': 'application/json'}
|
||||
|
||||
request, response = sanic_endpoint_test(app, data=json_dumps(payload), headers=headers)
|
||||
request, response = app.test_client.get(
|
||||
'/',
|
||||
data=json_dumps(payload), headers=headers)
|
||||
|
||||
assert request.json.get('test') == '✓'
|
||||
assert response.text == 'OK'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
|
||||
def test_vhosts():
|
||||
@@ -15,11 +14,11 @@ def test_vhosts():
|
||||
return text("You're at subdomain.example.com!")
|
||||
|
||||
headers = {"Host": "example.com"}
|
||||
request, response = sanic_endpoint_test(app, headers=headers)
|
||||
request, response = app.test_client.get('/', headers=headers)
|
||||
assert response.text == "You're at example.com!"
|
||||
|
||||
headers = {"Host": "subdomain.example.com"}
|
||||
request, response = sanic_endpoint_test(app, headers=headers)
|
||||
request, response = app.test_client.get('/', headers=headers)
|
||||
assert response.text == "You're at subdomain.example.com!"
|
||||
|
||||
|
||||
@@ -31,9 +30,27 @@ def test_vhosts_with_list():
|
||||
return text("Hello, world!")
|
||||
|
||||
headers = {"Host": "hello.com"}
|
||||
request, response = sanic_endpoint_test(app, headers=headers)
|
||||
request, response = app.test_client.get('/', headers=headers)
|
||||
assert response.text == "Hello, world!"
|
||||
|
||||
headers = {"Host": "world.com"}
|
||||
request, response = sanic_endpoint_test(app, headers=headers)
|
||||
request, response = app.test_client.get('/', headers=headers)
|
||||
assert response.text == "Hello, world!"
|
||||
|
||||
def test_vhosts_with_defaults():
|
||||
app = Sanic('test_vhosts')
|
||||
|
||||
@app.route('/', host="hello.com")
|
||||
async def handler(request):
|
||||
return text("Hello, world!")
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
return text("default")
|
||||
|
||||
headers = {"Host": "hello.com"}
|
||||
request, response = app.test_client.get('/', headers=headers)
|
||||
assert response.text == "Hello, world!"
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.text == "default"
|
||||
|
||||
@@ -1,43 +1,45 @@
|
||||
import pytest as pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.response import text, HTTPResponse
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.views import HTTPMethodView, CompositionView
|
||||
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 = getattr(app.test_client, method.lower())('/')
|
||||
assert response.headers['method'] == method
|
||||
|
||||
|
||||
def test_unexisting_methods():
|
||||
@@ -49,9 +51,9 @@ def test_unexisting_methods():
|
||||
return text('I am get method')
|
||||
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
request, response = sanic_endpoint_test(app, method="get")
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.text == 'I am get method'
|
||||
request, response = sanic_endpoint_test(app, method="post")
|
||||
request, response = app.test_client.post('/')
|
||||
assert response.text == 'Error: Method POST not allowed for URL /'
|
||||
|
||||
|
||||
@@ -65,7 +67,7 @@ def test_argument_methods():
|
||||
|
||||
app.add_route(DummyView.as_view(), '/<my_param_here>')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test123')
|
||||
request, response = app.test_client.get('/test123')
|
||||
|
||||
assert response.text == 'I am get method with test123'
|
||||
|
||||
@@ -82,7 +84,7 @@ def test_with_bp():
|
||||
bp.add_route(DummyView.as_view(), '/')
|
||||
|
||||
app.blueprint(bp)
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.text == 'I am get method'
|
||||
|
||||
@@ -99,7 +101,7 @@ def test_with_bp_with_url_prefix():
|
||||
bp.add_route(DummyView.as_view(), '/')
|
||||
|
||||
app.blueprint(bp)
|
||||
request, response = sanic_endpoint_test(app, uri='/test1/')
|
||||
request, response = app.test_client.get('/test1/')
|
||||
|
||||
assert response.text == 'I am get method'
|
||||
|
||||
@@ -120,7 +122,7 @@ def test_with_middleware():
|
||||
async def handler(request):
|
||||
results.append(request)
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.text == 'I am get method'
|
||||
assert type(results[0]) is Request
|
||||
@@ -147,7 +149,7 @@ def test_with_middleware_response():
|
||||
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.text == 'I am get method'
|
||||
assert type(results[0]) is Request
|
||||
@@ -169,7 +171,7 @@ def test_with_custom_class_methods():
|
||||
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")
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.text == 'I am get method and global var is 10'
|
||||
|
||||
|
||||
@@ -191,6 +193,68 @@ def test_with_decorator():
|
||||
return text('I am get method')
|
||||
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
request, response = sanic_endpoint_test(app, method="get")
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.text == 'I am get method'
|
||||
assert results[0] == 1
|
||||
|
||||
|
||||
def test_composition_view_rejects_incorrect_methods():
|
||||
def foo(request):
|
||||
return text('Foo')
|
||||
|
||||
view = CompositionView()
|
||||
|
||||
with pytest.raises(InvalidUsage) as e:
|
||||
view.add(['GET', 'FOO'], foo)
|
||||
|
||||
assert str(e.value) == 'FOO is not a valid HTTP method.'
|
||||
|
||||
|
||||
def test_composition_view_rejects_duplicate_methods():
|
||||
def foo(request):
|
||||
return text('Foo')
|
||||
|
||||
view = CompositionView()
|
||||
|
||||
with pytest.raises(InvalidUsage) as e:
|
||||
view.add(['GET', 'POST', 'GET'], foo)
|
||||
|
||||
assert str(e.value) == 'Method GET is already registered.'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', HTTP_METHODS)
|
||||
def test_composition_view_runs_methods_as_expected(method):
|
||||
app = Sanic('test_composition_view')
|
||||
|
||||
view = CompositionView()
|
||||
view.add(['GET', 'POST', 'PUT'], lambda x: text('first method'))
|
||||
view.add(['DELETE', 'PATCH'], lambda x: text('second method'))
|
||||
|
||||
app.add_route(view, '/')
|
||||
|
||||
if method in ['GET', 'POST', 'PUT']:
|
||||
request, response = getattr(app.test_client, method.lower())('/')
|
||||
assert response.text == 'first method'
|
||||
|
||||
if method in ['DELETE', 'PATCH']:
|
||||
request, response = getattr(app.test_client, method.lower())('/')
|
||||
assert response.text == 'second method'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', HTTP_METHODS)
|
||||
def test_composition_view_rejects_invalid_methods(method):
|
||||
app = Sanic('test_composition_view')
|
||||
|
||||
view = CompositionView()
|
||||
view.add(['GET', 'POST', 'PUT'], lambda x: text('first method'))
|
||||
|
||||
app.add_route(view, '/')
|
||||
|
||||
if method in ['GET', 'POST', 'PUT']:
|
||||
request, response = getattr(app.test_client, method.lower())('/')
|
||||
assert response.status == 200
|
||||
assert response.text == 'first method'
|
||||
|
||||
if method in ['DELETE', 'PATCH']:
|
||||
request, response = getattr(app.test_client, method.lower())('/')
|
||||
assert response.status == 405
|
||||
|
||||
10
tox.ini
10
tox.ini
@@ -1,22 +1,26 @@
|
||||
[tox]
|
||||
|
||||
envlist = py35, py36, flake8
|
||||
|
||||
[travis]
|
||||
|
||||
[travis]
|
||||
python =
|
||||
3.5: py35, flake8
|
||||
3.6: py36, flake8
|
||||
|
||||
[testenv]
|
||||
|
||||
[testenv]
|
||||
deps =
|
||||
aiofiles
|
||||
aiohttp
|
||||
pytest
|
||||
beautifulsoup4
|
||||
coverage
|
||||
|
||||
commands =
|
||||
pytest tests {posargs}
|
||||
coverage erase
|
||||
coverage run -m sanic.app
|
||||
coverage report
|
||||
|
||||
[testenv:flake8]
|
||||
deps =
|
||||
|
||||
Reference in New Issue
Block a user