Compare commits
146 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e5f513088 | ||
|
|
9fcf725061 | ||
|
|
601e015f00 | ||
|
|
21fb1dff7e | ||
|
|
da924a359c | ||
|
|
a5066f15dc | ||
|
|
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 | ||
|
|
d193a1eb70 |
@@ -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/*
|
||||
|
||||
@@ -78,10 +78,7 @@ Documentation
|
||||
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 :(
|
||||
|
||||
@@ -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,8 +153,8 @@ 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)
|
||||
```
|
||||
@@ -167,7 +167,7 @@ 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)
|
||||
url = app.url_for('v1.post_handler', post_id=5) # --> '/v1/post/5'
|
||||
return redirect(url)
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -5,5 +5,10 @@ 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.
|
||||
- [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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +11,7 @@ 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
|
||||
@@ -64,9 +64,9 @@ async def folder_handler(request, folder_id):
|
||||
|
||||
## HTTP request types
|
||||
|
||||
By default, a route defined on a URL will be avaialble for only GET requests to that URL.
|
||||
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`,
|
||||
whicl allows the handler function to work with any of the HTTP methods in the list.
|
||||
which allows the handler function to work with any of the HTTP methods in the list.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
@@ -81,6 +81,19 @@ async def get_handler(request):
|
||||
|
||||
```
|
||||
|
||||
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
|
||||
@@ -145,9 +158,26 @@ Other things to keep in mind when using `url_for`:
|
||||
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.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,51 +1,73 @@
|
||||
# Testing
|
||||
|
||||
Sanic endpoints can be tested locally using the `sanic.utils` module, which
|
||||
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 `sanic_endpoint_test` function runs a local server, issues a
|
||||
configurable request to an endpoint, and returns the result. It takes the
|
||||
following arguments:
|
||||
library.
|
||||
|
||||
- `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.
|
||||
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.
|
||||
- `loop` *(default `None`)* The event loop to use.
|
||||
- `debug` *(default `False`)* A boolean which determines whether to run the
|
||||
server in debug mode.
|
||||
- `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 with a GET request, `method` would be `get` and the keyword
|
||||
argument `params={'value', 'key'}` would be supplied. More information about
|
||||
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).
|
||||
|
||||
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
|
||||
### 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
|
||||
|
||||
# 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']
|
||||
def test_index_returns_200():
|
||||
request, response = sanic_endpoint_test(app)
|
||||
assert response.status == 200
|
||||
```
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.1'
|
||||
__version__ = '0.4.1'
|
||||
|
||||
__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')
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import logging
|
||||
import re
|
||||
import warnings
|
||||
from asyncio import get_event_loop
|
||||
from collections import deque
|
||||
from collections import deque, defaultdict
|
||||
from functools import partial
|
||||
from inspect import isawaitable, stack, getmodulename
|
||||
import re
|
||||
from traceback import format_exc
|
||||
from urllib.parse import urlencode, urlunparse
|
||||
import warnings
|
||||
|
||||
from .config import Config
|
||||
from .constants import HTTP_METHODS
|
||||
from .exceptions import Handler
|
||||
from .exceptions import ServerError, URLBuildError
|
||||
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
|
||||
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):
|
||||
|
||||
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:
|
||||
@@ -31,12 +33,15 @@ class Sanic:
|
||||
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 Handler()
|
||||
self.error_handler = error_handler or ErrorHandler()
|
||||
self.config = Config()
|
||||
self.request_middleware = deque()
|
||||
self.response_middleware = deque()
|
||||
@@ -44,22 +49,61 @@ class Sanic:
|
||||
self._blueprint_order = []
|
||||
self.debug = None
|
||||
self.sock = None
|
||||
self.processes = 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):
|
||||
"""
|
||||
Decorates a function to be registered as a route
|
||||
"""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
|
||||
"""
|
||||
|
||||
@@ -77,29 +121,28 @@ class Sanic:
|
||||
|
||||
# Shorthand method decorators
|
||||
def get(self, uri, host=None):
|
||||
return self.route(uri, methods=["GET"], host=host)
|
||||
return self.route(uri, methods=frozenset({"GET"}), host=host)
|
||||
|
||||
def post(self, uri, host=None):
|
||||
return self.route(uri, methods=["POST"], host=host)
|
||||
return self.route(uri, methods=frozenset({"POST"}), host=host)
|
||||
|
||||
def put(self, uri, host=None):
|
||||
return self.route(uri, methods=["PUT"], host=host)
|
||||
return self.route(uri, methods=frozenset({"PUT"}), host=host)
|
||||
|
||||
def head(self, uri, host=None):
|
||||
return self.route(uri, methods=["HEAD"], host=host)
|
||||
return self.route(uri, methods=frozenset({"HEAD"}), host=host)
|
||||
|
||||
def options(self, uri, host=None):
|
||||
return self.route(uri, methods=["OPTIONS"], host=host)
|
||||
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host)
|
||||
|
||||
def patch(self, uri, host=None):
|
||||
return self.route(uri, methods=["PATCH"], host=host)
|
||||
return self.route(uri, methods=frozenset({"PATCH"}), host=host)
|
||||
|
||||
def delete(self, uri, host=None):
|
||||
return self.route(uri, methods=["DELETE"], host=host)
|
||||
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
|
||||
"""A helper method to register class instance or
|
||||
functions as a handler to the application url
|
||||
routes.
|
||||
|
||||
@@ -107,11 +150,21 @@ class Sanic:
|
||||
: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 = frozenset(HTTP_METHODS)
|
||||
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
|
||||
|
||||
@@ -120,10 +173,9 @@ class Sanic:
|
||||
|
||||
# Decorator
|
||||
def exception(self, *exceptions):
|
||||
"""
|
||||
Decorates a function to be registered as a handler for exceptions
|
||||
"""Decorate a function to be registered as a handler for exceptions
|
||||
|
||||
:param \*exceptions: exceptions
|
||||
:param exceptions: exceptions
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
@@ -135,14 +187,11 @@ class Sanic:
|
||||
return response
|
||||
|
||||
# Decorator
|
||||
def middleware(self, *args, **kwargs):
|
||||
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')
|
||||
"""
|
||||
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):
|
||||
def register_middleware(middleware, attach_to='request'):
|
||||
if attach_to == 'request':
|
||||
self.request_middleware.append(middleware)
|
||||
if attach_to == 'response':
|
||||
@@ -150,25 +199,24 @@ class Sanic:
|
||||
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])
|
||||
if callable(middleware_or_request):
|
||||
return register_middleware(middleware_or_request)
|
||||
|
||||
else:
|
||||
attach_to = args[0]
|
||||
return register_middleware
|
||||
return partial(register_middleware,
|
||||
attach_to=middleware_or_request)
|
||||
|
||||
# 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
|
||||
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_modified_since, use_content_range)
|
||||
|
||||
def blueprint(self, blueprint, **options):
|
||||
"""
|
||||
Registers a blueprint on the application.
|
||||
"""Register a blueprint on the application.
|
||||
|
||||
:param blueprint: Blueprint object
|
||||
:param options: option dictionary with blueprint defaults
|
||||
@@ -195,7 +243,7 @@ class Sanic:
|
||||
return self.blueprint(*args, **kwargs)
|
||||
|
||||
def url_for(self, view_name: str, **kwargs):
|
||||
"""Builds a URL based on a view name and the values provided.
|
||||
"""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
|
||||
@@ -205,7 +253,7 @@ class Sanic:
|
||||
Keyword arguments that are not request parameters will be included in
|
||||
the output URL's query string.
|
||||
|
||||
:param view_name: A string referencing the view name
|
||||
:param view_name: string referencing the view name
|
||||
:param **kwargs: keys and values that are used to build request
|
||||
parameters and query string arguments.
|
||||
|
||||
@@ -222,12 +270,28 @@ class Sanic:
|
||||
'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)
|
||||
@@ -268,12 +332,9 @@ class Sanic:
|
||||
replacement_regex, supplied_param, out)
|
||||
|
||||
# parse the remainder of the keyword arguments into a querystring
|
||||
if kwargs:
|
||||
query_string = urlencode(kwargs)
|
||||
out = urlunparse((
|
||||
'', '', out,
|
||||
'', query_string, ''
|
||||
))
|
||||
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
|
||||
|
||||
@@ -285,9 +346,8 @@ class Sanic:
|
||||
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
|
||||
"""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
|
||||
@@ -300,6 +360,8 @@ class Sanic:
|
||||
# Request Middleware
|
||||
# -------------------------------------------- #
|
||||
|
||||
request.app = self
|
||||
|
||||
response = False
|
||||
# The if improves speed. I don't know why
|
||||
if self.request_middleware:
|
||||
@@ -361,6 +423,14 @@ class Sanic:
|
||||
|
||||
response_callback(response)
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Testing
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
@property
|
||||
def test_client(self):
|
||||
return TestClient(self)
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Execution
|
||||
# -------------------------------------------------------------------- #
|
||||
@@ -369,9 +439,8 @@ class Sanic:
|
||||
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.
|
||||
"""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
|
||||
@@ -388,6 +457,10 @@ class Sanic:
|
||||
: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
|
||||
"""
|
||||
@@ -397,16 +470,18 @@ class Sanic:
|
||||
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 Exception as e:
|
||||
except:
|
||||
log.exception(
|
||||
'Experienced exception while trying to serve')
|
||||
|
||||
finally:
|
||||
self.is_running = False
|
||||
log.info("Server Stopped")
|
||||
|
||||
def stop(self):
|
||||
@@ -418,22 +493,19 @@ class Sanic:
|
||||
before_stop=None, after_stop=None, ssl=None,
|
||||
sock=None, loop=None, protocol=HttpProtocol,
|
||||
backlog=100, stop_event=None):
|
||||
"""
|
||||
Asynchronous version of `run`.
|
||||
"""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,
|
||||
protocol=protocol, backlog=backlog, stop_event=stop_event,
|
||||
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)
|
||||
|
||||
# Serve
|
||||
proto = "http"
|
||||
if ssl is not None:
|
||||
proto = "https"
|
||||
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
|
||||
return await serve(**server_settings)
|
||||
|
||||
def _helper(self, host="127.0.0.1", port=8000, debug=False,
|
||||
@@ -441,9 +513,7 @@ class Sanic:
|
||||
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`.
|
||||
"""
|
||||
"""Helper function used by `run` and `create_server`."""
|
||||
|
||||
if loop is not None:
|
||||
if debug:
|
||||
@@ -453,9 +523,18 @@ class 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
|
||||
self.loop = loop = get_event_loop()
|
||||
|
||||
server_settings = {
|
||||
'protocol': protocol,
|
||||
@@ -477,19 +556,18 @@ class Sanic:
|
||||
# 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]
|
||||
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):
|
||||
args = [args]
|
||||
listeners += args
|
||||
listeners.append(args)
|
||||
else:
|
||||
listeners.extend(args)
|
||||
if reverse:
|
||||
listeners.reverse()
|
||||
# Prepend sanic to the arguments when listeners are triggered
|
||||
@@ -1,5 +1,7 @@
|
||||
from collections import defaultdict, namedtuple
|
||||
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.views import CompositionView
|
||||
|
||||
FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host'])
|
||||
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
|
||||
@@ -11,9 +13,9 @@ FutureStatic = namedtuple('Route',
|
||||
|
||||
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
|
||||
@@ -27,9 +29,7 @@ class Blueprint:
|
||||
self.statics = []
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Registers the blueprint to the sanic app.
|
||||
"""
|
||||
"""Register the blueprint to the sanic app."""
|
||||
|
||||
url_prefix = options.get('url_prefix', self.url_prefix)
|
||||
|
||||
@@ -41,7 +41,7 @@ class Blueprint:
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
app.route(
|
||||
uri=uri,
|
||||
uri=uri[1:] if uri.startswith('//') else uri,
|
||||
methods=future.methods,
|
||||
host=future.host or self.host
|
||||
)(future.handler)
|
||||
@@ -65,11 +65,16 @@ class Blueprint:
|
||||
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):
|
||||
route = FutureRoute(handler, uri, methods, host)
|
||||
@@ -77,20 +82,33 @@ class Blueprint:
|
||||
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.
|
||||
"""
|
||||
route = FutureRoute(handler, uri, methods, host)
|
||||
self.routes.append(route)
|
||||
# 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):
|
||||
@@ -99,9 +117,7 @@ class Blueprint:
|
||||
return decorator
|
||||
|
||||
def middleware(self, *args, **kwargs):
|
||||
"""
|
||||
Creates a blueprint middleware from a decorated function.
|
||||
"""
|
||||
"""Create a blueprint middleware from a decorated function."""
|
||||
def register_middleware(_middleware):
|
||||
future_middleware = FutureMiddleware(_middleware, args, kwargs)
|
||||
self.middlewares.append(future_middleware)
|
||||
@@ -116,9 +132,7 @@ 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):
|
||||
exception = FutureException(handler, args, kwargs)
|
||||
self.exceptions.append(exception)
|
||||
@@ -126,9 +140,9 @@ class Blueprint:
|
||||
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.
|
||||
"""
|
||||
static = FutureStatic(uri, file_or_directory, args, kwargs)
|
||||
|
||||
@@ -39,8 +39,9 @@ class Config(dict):
|
||||
self[attr] = value
|
||||
|
||||
def from_envvar(self, variable_name):
|
||||
"""Loads a configuration from an environment variable pointing to
|
||||
"""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.
|
||||
"""
|
||||
@@ -52,8 +53,9 @@ class Config(dict):
|
||||
return self.from_pyfile(config_file)
|
||||
|
||||
def from_pyfile(self, filename):
|
||||
"""Updates the values in the config from a Python file. Only the uppercase
|
||||
variables in that module are stored in the config.
|
||||
"""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')
|
||||
@@ -69,7 +71,7 @@ class Config(dict):
|
||||
return True
|
||||
|
||||
def from_object(self, obj):
|
||||
"""Updates the values from the given object.
|
||||
"""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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -141,85 +138,20 @@ class PayloadTooLarge(SanicException):
|
||||
status_code = 413
|
||||
|
||||
|
||||
class Handler:
|
||||
handlers = None
|
||||
cached_handlers = None
|
||||
_missing = object()
|
||||
class HeaderNotFound(SanicException):
|
||||
status_code = 400
|
||||
|
||||
def __init__(self):
|
||||
self.handlers = []
|
||||
self.cached_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.append((exception, handler))
|
||||
|
||||
def lookup(self, exception):
|
||||
handler = self.cached_handlers.get(exception, self._missing)
|
||||
if handler is self._missing:
|
||||
for exception_class, handler in self.handlers:
|
||||
if isinstance(exception, exception_class):
|
||||
self.cached_handlers[type(exception)] = handler
|
||||
return handler
|
||||
self.cached_handlers[type(exception)] = None
|
||||
handler = None
|
||||
return handler
|
||||
|
||||
def response(self, request, exception):
|
||||
"""
|
||||
Fetches and executes an exception handler and returns a response object
|
||||
|
||||
:param request: Request
|
||||
:param exception: Exception to handle
|
||||
:return: Response object
|
||||
"""
|
||||
handler = self.lookup(exception)
|
||||
try:
|
||||
response = handler and handler(
|
||||
request=request, exception=exception)
|
||||
if response is None:
|
||||
response = self.default(request=request, exception=exception)
|
||||
except:
|
||||
log.error(format_exc())
|
||||
if self.debug:
|
||||
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 {}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import re
|
||||
from collections import defaultdict, namedtuple
|
||||
from collections.abc import Iterable
|
||||
from functools import lru_cache
|
||||
from .exceptions import NotFound, InvalidUsage
|
||||
from .views import CompositionView
|
||||
|
||||
from sanic.exceptions import NotFound, InvalidUsage
|
||||
from sanic.views import CompositionView
|
||||
|
||||
Route = namedtuple(
|
||||
'Route',
|
||||
@@ -32,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:
|
||||
|
||||
@@ -68,13 +69,14 @@ class Router:
|
||||
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
|
||||
"""Parse a parameter string into its constituent name, type, and
|
||||
pattern
|
||||
|
||||
For example:
|
||||
`parse_parameter_string('<param_one:[A-z]')` ->
|
||||
`parse_parameter_string('<param_one:[A-z]>')` ->
|
||||
('param_one', str, '[A-z]')
|
||||
|
||||
:param parameter_string: String to parse
|
||||
@@ -94,33 +96,50 @@ class Router:
|
||||
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] == '/'
|
||||
and not self.routes_all.get(uri + '/', False)
|
||||
)
|
||||
without_slash_is_missing = (
|
||||
uri[-1] == '/'
|
||||
and not self.routes_all.get(uri[:-1], False)
|
||||
and not uri == '/'
|
||||
)
|
||||
# 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:
|
||||
@@ -239,8 +258,7 @@ class Router:
|
||||
|
||||
@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.
|
||||
"""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)
|
||||
@@ -255,26 +273,30 @@ class Router:
|
||||
return (None, None)
|
||||
|
||||
def get(self, request):
|
||||
"""
|
||||
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
|
||||
|
||||
: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=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
|
||||
|
||||
@@ -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
|
||||
@@ -9,21 +10,19 @@ from signal import SIGTERM, SIGINT
|
||||
from signal import signal as signal_func
|
||||
from socket import socket, SOL_SOCKET, SO_REUSEADDR
|
||||
from time import time
|
||||
import warnings
|
||||
|
||||
from httptools import HttpRequestParser
|
||||
from httptools.parser.errors import HttpParserError
|
||||
|
||||
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
|
||||
|
||||
@@ -33,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
|
||||
"""
|
||||
@@ -169,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()
|
||||
@@ -198,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()
|
||||
@@ -228,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:
|
||||
@@ -239,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:
|
||||
@@ -252,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,
|
||||
@@ -270,31 +272,30 @@ 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
|
||||
"""
|
||||
if not run_async:
|
||||
@@ -346,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))
|
||||
@@ -376,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
|
||||
|
||||
@@ -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,59 +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.lower())(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 = [None, None]
|
||||
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):
|
||||
if results[0] is None:
|
||||
results[0] = request
|
||||
app.request_middleware.appendleft(_collect_request)
|
||||
|
||||
async def _collect_response(sanic, loop):
|
||||
try:
|
||||
response = await local_request(method, uri, *request_args,
|
||||
**request_kwargs)
|
||||
results[-1] = 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[-1]
|
||||
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)
|
||||
@@ -69,7 +65,7 @@ class HTTPMethodView:
|
||||
|
||||
|
||||
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.
|
||||
|
||||
@@ -89,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)
|
||||
|
||||
19
setup.py
19
setup.py
@@ -15,6 +15,15 @@ with codecs.open(os.path.join(os.path.abspath(os.path.dirname(
|
||||
except IndexError:
|
||||
raise RuntimeError('Unable to determine version.')
|
||||
|
||||
install_requires = [
|
||||
'httptools>=0.0.9',
|
||||
'ujson>=1.35',
|
||||
'aiofiles>=0.3.0',
|
||||
]
|
||||
|
||||
if os.name != 'nt':
|
||||
install_requires.append('uvloop>=0.5.3')
|
||||
|
||||
setup(
|
||||
name='sanic',
|
||||
version=version,
|
||||
@@ -22,15 +31,11 @@ setup(
|
||||
license='MIT',
|
||||
author='Channel Cat',
|
||||
author_email='channelcat@gmail.com',
|
||||
description='A microframework based on uvloop, httptools, and learnings of flask',
|
||||
description=(
|
||||
'A microframework based on uvloop, httptools, and learnings of flask'),
|
||||
packages=['sanic'],
|
||||
platforms='any',
|
||||
install_requires=[
|
||||
'uvloop>=0.5.3',
|
||||
'httptools>=0.0.9',
|
||||
'ujson>=1.35',
|
||||
'aiofiles>=0.3.0',
|
||||
],
|
||||
install_requires=install_requires,
|
||||
classifiers=[
|
||||
'Development Status :: 2 - Pre-Alpha',
|
||||
'Environment :: Web Environment',
|
||||
|
||||
@@ -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,7 +229,7 @@ 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
|
||||
|
||||
@@ -263,44 +267,44 @@ def test_bp_shorthand():
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
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
|
||||
|
||||
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('/post')
|
||||
assert response.status == 405
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/options', method='options')
|
||||
request, response = app.test_client.options('/options')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/options', method='get')
|
||||
request, response = app.test_client.get('/options')
|
||||
assert response.status == 405
|
||||
|
||||
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
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/delete', method='delete')
|
||||
request, response = app.test_client.delete('/delete')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/delete', method='get')
|
||||
request, response = app.test_client.get('/delete')
|
||||
assert response.status == 405
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.router import RouteExists
|
||||
import pytest
|
||||
|
||||
@@ -22,8 +21,7 @@ def test_overload_dynamic_routes(method, attr, expected):
|
||||
async def handler2(request, param):
|
||||
return text('OK2 ' + param)
|
||||
|
||||
request, response = sanic_endpoint_test(
|
||||
app, method, uri='/overload/test')
|
||||
request, response = getattr(app.test_client, method)('/overload/test')
|
||||
assert getattr(response, attr) == expected
|
||||
|
||||
|
||||
|
||||
@@ -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')
|
||||
@@ -31,7 +30,7 @@ def handler_4(request):
|
||||
@exception_handler_app.route('/5')
|
||||
def handler_5(request):
|
||||
class CustomServerError(ServerError):
|
||||
pass
|
||||
status_code=200
|
||||
raise CustomServerError('Custom server error')
|
||||
|
||||
|
||||
@@ -41,31 +40,30 @@ def handler_exception(request, exception):
|
||||
|
||||
|
||||
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)
|
||||
@@ -81,5 +79,5 @@ def test_html_traceback_output_in_debug_mode():
|
||||
|
||||
|
||||
def test_inherited_exception_handler():
|
||||
request, response = sanic_endpoint_test(exception_handler_app, uri='/5')
|
||||
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'
|
||||
|
||||
@@ -2,7 +2,6 @@ import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text, redirect
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -40,9 +39,8 @@ 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",
|
||||
request, response = redirect_app.test_client.get(
|
||||
'/redirect_init',
|
||||
allow_redirects=False)
|
||||
|
||||
assert response.status == 302
|
||||
@@ -51,8 +49,7 @@ def test_redirect_default_302(redirect_app):
|
||||
|
||||
|
||||
def test_redirect_headers_none(redirect_app):
|
||||
request, response = sanic_endpoint_test(
|
||||
redirect_app, method="get",
|
||||
request, response = redirect_app.test_client.get(
|
||||
uri="/redirect_init",
|
||||
headers=None,
|
||||
allow_redirects=False)
|
||||
@@ -65,9 +62,8 @@ 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",
|
||||
request, response = redirect_app.test_client.get(
|
||||
"/redirect_init_with_301",
|
||||
allow_redirects=False)
|
||||
|
||||
assert response.status == 301
|
||||
@@ -78,9 +74,8 @@ 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,
|
||||
request, response = redirect_app.test_client.get(
|
||||
"/redirect_init",
|
||||
allow_redirects=True)
|
||||
|
||||
assert response.status == 200
|
||||
@@ -88,8 +83,8 @@ def test_get_then_redirect_follow_redirect(redirect_app):
|
||||
|
||||
|
||||
def test_chained_redirect(redirect_app):
|
||||
"""Test sanic_endpoint_test is working for redirection"""
|
||||
request, response = sanic_endpoint_test(redirect_app, uri='/1')
|
||||
"""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'
|
||||
|
||||
@@ -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,7 +56,7 @@ 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'
|
||||
|
||||
@@ -70,7 +71,7 @@ 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."
|
||||
|
||||
@@ -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,6 +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'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -486,6 +498,19 @@ def test_remove_inexistent_route():
|
||||
with pytest.raises(RouteDoesNotExist):
|
||||
app.remove_route('/test')
|
||||
|
||||
def test_removing_slash():
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.get('/rest/<resource>')
|
||||
def get(_):
|
||||
pass
|
||||
|
||||
@app.post('/rest/<resource>')
|
||||
def post(_):
|
||||
pass
|
||||
|
||||
assert len(app.router.routes_all.keys()) == 2
|
||||
|
||||
|
||||
def test_remove_unhashable_route():
|
||||
app = Sanic('test_remove_unhashable_route')
|
||||
@@ -495,24 +520,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 +549,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 +579,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 +609,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 +625,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')
|
||||
@@ -32,7 +31,7 @@ def test_static_file(static_file_directory, file_name):
|
||||
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 == get_file_content(static_file_directory, file_name)
|
||||
|
||||
@@ -44,7 +43,121 @@ def test_static_directory(file_name, base_uri, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
app.static(base_uri, static_file_directory)
|
||||
|
||||
request, response = sanic_endpoint_test(
|
||||
app, uri='{}/{}'.format(base_uri, file_name))
|
||||
request, response = app.test_client.get(
|
||||
uri='{}/{}'.format(base_uri, file_name))
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(static_file_directory, file_name)
|
||||
|
||||
|
||||
@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(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
request, response = app.test_client.head('/testing.file')
|
||||
assert response.status == 200
|
||||
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)),)
|
||||
|
||||
@@ -5,11 +5,19 @@ from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.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:
|
||||
@@ -33,12 +41,28 @@ def test_simple_url_for_getting(simple_app):
|
||||
url = simple_app.url_for(letter)
|
||||
|
||||
assert url == '/{}'.format(letter)
|
||||
request, response = sanic_endpoint_test(
|
||||
simple_app, uri=url)
|
||||
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')
|
||||
|
||||
@@ -75,6 +99,19 @@ def test_fails_url_build_if_param_not_passed():
|
||||
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>')
|
||||
@@ -179,11 +216,11 @@ def blueprint_app():
|
||||
return text(
|
||||
'foo from first : {}'.format(param))
|
||||
|
||||
@second_print.route('/foo') # noqa
|
||||
@second_print.route('/foo') # noqa
|
||||
def foo():
|
||||
return text('foo from second')
|
||||
|
||||
@second_print.route('/foo/<param>') # noqa
|
||||
@second_print.route('/foo/<param>') # noqa
|
||||
def foo_with_param(request, param):
|
||||
return text(
|
||||
'foo from second : {}'.format(param))
|
||||
|
||||
@@ -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,11 +1,11 @@
|
||||
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
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ def test_methods(method):
|
||||
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
|
||||
request, response = sanic_endpoint_test(app, method=method)
|
||||
request, response = getattr(app.test_client, method.lower())('/')
|
||||
assert response.headers['method'] == method
|
||||
|
||||
|
||||
@@ -51,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 /'
|
||||
|
||||
|
||||
@@ -67,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'
|
||||
|
||||
@@ -84,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'
|
||||
|
||||
@@ -101,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'
|
||||
|
||||
@@ -122,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
|
||||
@@ -149,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
|
||||
@@ -171,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'
|
||||
|
||||
|
||||
@@ -193,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
|
||||
|
||||
Reference in New Issue
Block a user