Merge branch 'master' into protocol
This commit is contained in:
commit
ee8f8c2930
12
.travis.yml
12
.travis.yml
|
@ -1,14 +1,10 @@
|
|||
sudo: false
|
||||
language: python
|
||||
python:
|
||||
- '3.5'
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install -r requirements-dev.txt
|
||||
- python setup.py install
|
||||
- pip install flake8
|
||||
- pip install pytest
|
||||
before_script: flake8 sanic
|
||||
script: py.test -v tests
|
||||
- '3.6'
|
||||
install: pip install tox-travis
|
||||
script: tox
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: channelcat
|
||||
|
|
|
@ -60,6 +60,7 @@ if __name__ == "__main__":
|
|||
* [Cookies](docs/cookies.md)
|
||||
* [Static Files](docs/static_files.md)
|
||||
* [Custom Protocol](docs/custom_protocol.md)
|
||||
* [Testing](docs/testing.md)
|
||||
* [Deploying](docs/deploying.md)
|
||||
* [Contributing](docs/contributing.md)
|
||||
* [License](LICENSE)
|
||||
|
|
|
@ -6,6 +6,7 @@ Sanic has simple class based implementation. You should implement methods(get, p
|
|||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic('some_name')
|
||||
|
||||
|
|
|
@ -27,3 +27,23 @@ async def handler(request):
|
|||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
## Middleware chain
|
||||
|
||||
If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that you do **not return** any response in your middleware:
|
||||
|
||||
```python
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.middleware('response')
|
||||
async def custom_banner(request, response):
|
||||
response.headers["Server"] = "Fake-Server"
|
||||
|
||||
@app.middleware('response')
|
||||
async def prevent_xss(request, response):
|
||||
response.headers["x-xss-protection"] = "1; mode=block"
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
The above code will apply the two middlewares in order. First the middleware **custom_banner** will change the HTTP Response headers *Server* by *Fake-Server*, and the second middleware **prevent_xss** will add the HTTP Headers for prevent Cross-Site-Scripting (XSS) attacks.
|
||||
|
|
51
docs/testing.md
Normal file
51
docs/testing.md
Normal file
|
@ -0,0 +1,51 @@
|
|||
# Testing
|
||||
|
||||
Sanic endpoints can be tested locally using the `sanic.utils` module, which
|
||||
depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
|
||||
library. The `sanic_endpoint_test` function runs a local server, issues a
|
||||
configurable request to an endpoint, and returns the result. It takes the
|
||||
following arguments:
|
||||
|
||||
- `app` An instance of a Sanic app.
|
||||
- `method` *(default `'get'`)* A string representing the HTTP method to use.
|
||||
- `uri` *(default `'/'`)* A string representing the endpoint to test.
|
||||
- `gather_request` *(default `True`)* A boolean which determines whether the
|
||||
original request will be returned by the function. If set to `True`, the
|
||||
return value is a tuple of `(request, response)`, if `False` only the
|
||||
response is returned.
|
||||
- `loop` *(default `None`)* The event loop to use.
|
||||
- `debug` *(default `False`)* A boolean which determines whether to run the
|
||||
server in debug mode.
|
||||
|
||||
The function further takes the `*request_args` and `**request_kwargs`, which
|
||||
are passed directly to the aiohttp ClientSession request. For example, to
|
||||
supply data with a GET request, `method` would be `get` and the keyword
|
||||
argument `params={'value', 'key'}` would be supplied. More information about
|
||||
the available arguments to aiohttp can be found
|
||||
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
|
||||
|
||||
Below is a complete example of an endpoint test,
|
||||
using [pytest](http://doc.pytest.org/en/latest/). The test checks that the
|
||||
`/challenge` endpoint responds to a GET request with a supplied challenge
|
||||
string.
|
||||
|
||||
```python
|
||||
import pytest
|
||||
import aiohttp
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
# Import the Sanic app, usually created with Sanic(__name__)
|
||||
from external_server import app
|
||||
|
||||
def test_endpoint_challenge():
|
||||
# Create the challenge data
|
||||
request_data = {'challenge': 'dummy_challenge'}
|
||||
|
||||
# Send the request to the endpoint, using the default `get` method
|
||||
request, response = sanic_endpoint_test(app,
|
||||
uri='/challenge',
|
||||
params=request_data)
|
||||
|
||||
# Assert that the server responds with the challenge string
|
||||
assert response.text == request_data['challenge']
|
||||
```
|
23
examples/override_logging.py
Normal file
23
examples/override_logging.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
import json
|
||||
import logging
|
||||
|
||||
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
|
||||
logging_format += "%(module)s::%(funcName)s():l%(lineno)d: "
|
||||
logging_format += "%(message)s"
|
||||
|
||||
logging.basicConfig(
|
||||
format=logging_format,
|
||||
level=logging.DEBUG
|
||||
)
|
||||
log = logging.getLogger()
|
||||
|
||||
# Set logger to override default basicConfig
|
||||
sanic = Sanic(logger=True)
|
||||
@sanic.route("/")
|
||||
def test(request):
|
||||
log.info("received request; responding with 'hey'")
|
||||
return text("hey")
|
||||
|
||||
sanic.run(host="0.0.0.0", port=8000)
|
|
@ -1,6 +1,6 @@
|
|||
from .sanic import Sanic
|
||||
from .blueprints import Blueprint
|
||||
|
||||
__version__ = '0.1.8'
|
||||
__version__ = '0.1.9'
|
||||
|
||||
__all__ = ['Sanic', 'Blueprint']
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s")
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
from aiofiles import open as open_async
|
||||
from .cookies import CookieJar
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
|
||||
from ujson import dumps as json_dumps
|
||||
|
||||
from .cookies import CookieJar
|
||||
|
||||
COMMON_STATUS_CODES = {
|
||||
200: b'OK',
|
||||
400: b'Bad Request',
|
||||
|
@ -79,7 +81,12 @@ class HTTPResponse:
|
|||
self.content_type = content_type
|
||||
|
||||
if body is not None:
|
||||
try:
|
||||
# Try to encode it regularly
|
||||
self.body = body.encode('utf-8')
|
||||
except AttributeError:
|
||||
# Convert it to a str if you can't
|
||||
self.body = str(body).encode('utf-8')
|
||||
else:
|
||||
self.body = body_bytes
|
||||
|
||||
|
|
|
@ -3,13 +3,14 @@ from collections import deque
|
|||
from functools import partial
|
||||
from inspect import isawaitable, stack, getmodulename
|
||||
from multiprocessing import Process, Event
|
||||
from select import select
|
||||
from signal import signal, SIGTERM, SIGINT
|
||||
from time import sleep
|
||||
from traceback import format_exc
|
||||
import logging
|
||||
|
||||
from .config import Config
|
||||
from .exceptions import Handler
|
||||
from .log import log, logging
|
||||
from .log import log
|
||||
from .response import HTTPResponse
|
||||
from .router import Router
|
||||
from .server import serve, HttpProtocol
|
||||
|
@ -18,7 +19,13 @@ from .exceptions import ServerError
|
|||
|
||||
|
||||
class Sanic:
|
||||
def __init__(self, name=None, router=None, error_handler=None):
|
||||
def __init__(self, name=None, router=None,
|
||||
error_handler=None, logger=None):
|
||||
if logger is None:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s: %(levelname)s: %(message)s"
|
||||
)
|
||||
if name is None:
|
||||
frame_records = stack()[1]
|
||||
name = getmodulename(frame_records[1])
|
||||
|
@ -347,8 +354,7 @@ class Sanic:
|
|||
|
||||
# Infinitely wait for the stop event
|
||||
try:
|
||||
while not stop_event.is_set():
|
||||
sleep(0.3)
|
||||
select(stop_event)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ from signal import SIGINT, SIGTERM
|
|||
from time import time
|
||||
from httptools import HttpRequestParser
|
||||
from httptools.parser.errors import HttpParserError
|
||||
from .exceptions import ServerError
|
||||
|
||||
try:
|
||||
import uvloop as async_loop
|
||||
|
@ -173,8 +174,9 @@ class HttpProtocol(asyncio.Protocol):
|
|||
"Writing error failed, connection closed {}".format(e))
|
||||
|
||||
def bail_out(self, message):
|
||||
log.debug(message)
|
||||
self.transport.close()
|
||||
exception = ServerError(message)
|
||||
self.write_error(exception)
|
||||
log.error(message)
|
||||
|
||||
def cleanup(self):
|
||||
self.parser = None
|
||||
|
|
|
@ -2,6 +2,7 @@ from aiofiles.os import stat
|
|||
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
|
||||
|
@ -32,12 +33,17 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
|
|||
# served. os.path.realpath seems to be very slow
|
||||
if file_uri and '../' in file_uri:
|
||||
raise InvalidUsage("Invalid URL")
|
||||
|
||||
# Merge served directory and requested file if provided
|
||||
# Strip all / that in the beginning of the URL to help prevent python
|
||||
# from herping a derp and treating the uri as an absolute path
|
||||
file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \
|
||||
if file_uri else file_or_directory
|
||||
file_path = file_or_directory
|
||||
if file_uri:
|
||||
file_path = path.join(
|
||||
file_or_directory, sub('^[/]*', '', file_uri))
|
||||
|
||||
# URL decode the path sent by the browser otherwise we won't be able to
|
||||
# match filenames which got encoded (filenames with spaces etc)
|
||||
file_path = unquote(file_path)
|
||||
try:
|
||||
headers = {}
|
||||
# Check if the client has been sent this file before
|
||||
|
|
|
@ -1,13 +1,27 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
type TestJSONResponse struct {
|
||||
Test bool
|
||||
}
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
|
||||
response := TestJSONResponse{true}
|
||||
|
||||
js, err := json.Marshal(response)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
1
tests/static/decode me.txt
Normal file
1
tests/static/decode me.txt
Normal file
|
@ -0,0 +1 @@
|
|||
I need to be decoded as a uri
|
1
tests/static/test.file
Normal file
1
tests/static/test.file
Normal file
|
@ -0,0 +1 @@
|
|||
I am just a regular static file
|
33
tests/test_logging.py
Normal file
33
tests/test_logging.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
import asyncio
|
||||
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; \
|
||||
function: %(funcName)s(); \
|
||||
message: %(message)s'''
|
||||
|
||||
def test_log():
|
||||
log_stream = StringIO()
|
||||
for handler in logging.root.handlers[:]:
|
||||
logging.root.removeHandler(handler)
|
||||
logging.basicConfig(
|
||||
format=logging_format,
|
||||
level=logging.DEBUG,
|
||||
stream=log_stream
|
||||
)
|
||||
log = logging.getLogger()
|
||||
app = Sanic('test_logging', logger=True)
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
log.info('hello world')
|
||||
return text('hello')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
log_text = log_stream.getvalue().strip().split('\n')[-3]
|
||||
assert log_text == "module: test_logging; function: handler(); message: hello world"
|
||||
|
||||
if __name__ =="__main__":
|
||||
test_log()
|
|
@ -1,5 +1,5 @@
|
|||
from multiprocessing import Array, Event, Process
|
||||
from time import sleep
|
||||
from time import sleep, time
|
||||
from ujson import loads as json_loads
|
||||
|
||||
from sanic import Sanic
|
||||
|
@ -51,3 +51,27 @@ def skip_test_multiprocessing():
|
|||
raise ValueError("Expected JSON response but got '{}'".format(response))
|
||||
|
||||
assert results.get('test') == True
|
||||
|
||||
|
||||
def test_drain_connections():
|
||||
app = Sanic('test_json')
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
return json({"test": True})
|
||||
|
||||
stop_event = Event()
|
||||
async def after_start(*args, **kwargs):
|
||||
http_response = await local_request('get', '/')
|
||||
stop_event.set()
|
||||
|
||||
start = time()
|
||||
app.serve_multiple({
|
||||
'host': HOST,
|
||||
'port': PORT,
|
||||
'after_start': after_start,
|
||||
'request_handler': app.handle_request,
|
||||
}, workers=2, stop_event=stop_event)
|
||||
end = time()
|
||||
|
||||
assert end - start < 0.05
|
||||
|
|
|
@ -2,6 +2,7 @@ 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
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
|
@ -32,6 +33,22 @@ def test_text():
|
|||
assert response.text == 'Hello'
|
||||
|
||||
|
||||
def test_invalid_response():
|
||||
app = Sanic('test_invalid_response')
|
||||
|
||||
@app.exception(ServerError)
|
||||
def handler_exception(request, exception):
|
||||
return text('Internal Server Error.', 500)
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
return 'This should fail'
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
assert response.status == 500
|
||||
assert response.text == "Internal Server Error."
|
||||
|
||||
|
||||
def test_json():
|
||||
app = Sanic('test_json')
|
||||
|
||||
|
|
18
tests/test_response.py
Normal file
18
tests/test_response.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
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():
|
||||
"""Test when a response body sent from the application is not a string"""
|
||||
app = Sanic('response_body_not_a_string')
|
||||
random_num = choice(range(1000))
|
||||
|
||||
@app.route('/hello')
|
||||
async def hello_route(request):
|
||||
return HTTPResponse(body=random_num)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/hello')
|
||||
assert response.text == str(random_num)
|
|
@ -1,30 +1,62 @@
|
|||
import inspect
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
def test_static_file():
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
with open(current_file, 'rb') as file:
|
||||
current_file_contents = file.read()
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def static_file_directory():
|
||||
"""The static directory to serve"""
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
current_directory = os.path.dirname(os.path.abspath(current_file))
|
||||
static_directory = os.path.join(current_directory, 'static')
|
||||
return static_directory
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def static_file_path(static_file_directory):
|
||||
"""The path to the static file that we want to serve"""
|
||||
return os.path.join(static_file_directory, 'test.file')
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def static_file_content(static_file_path):
|
||||
"""The content of the static file to check"""
|
||||
with open(static_file_path, 'rb') as file:
|
||||
return file.read()
|
||||
|
||||
|
||||
def test_static_file(static_file_path, static_file_content):
|
||||
app = Sanic('test_static')
|
||||
app.static('/testing.file', current_file)
|
||||
app.static('/testing.file', static_file_path)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/testing.file')
|
||||
assert response.status == 200
|
||||
assert response.body == current_file_contents
|
||||
assert response.body == static_file_content
|
||||
|
||||
def test_static_directory():
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
current_directory = os.path.dirname(os.path.abspath(current_file))
|
||||
with open(current_file, 'rb') as file:
|
||||
current_file_contents = file.read()
|
||||
|
||||
def test_static_directory(
|
||||
static_file_directory, static_file_path, static_file_content):
|
||||
|
||||
app = Sanic('test_static')
|
||||
app.static('/dir', current_directory)
|
||||
app.static('/dir', static_file_directory)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/dir/test_static.py')
|
||||
request, response = sanic_endpoint_test(app, uri='/dir/test.file')
|
||||
assert response.status == 200
|
||||
assert response.body == current_file_contents
|
||||
assert response.body == static_file_content
|
||||
|
||||
|
||||
def test_static_url_decode_file(static_file_directory):
|
||||
decode_me_path = os.path.join(static_file_directory, 'decode me.txt')
|
||||
with open(decode_me_path, 'rb') as file:
|
||||
decode_me_contents = file.read()
|
||||
|
||||
app = Sanic('test_static')
|
||||
app.static('/dir', static_file_directory)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/dir/decode me.txt')
|
||||
assert response.status == 200
|
||||
assert response.body == decode_me_contents
|
||||
|
|
33
tox.ini
33
tox.ini
|
@ -1,34 +1,25 @@
|
|||
[tox]
|
||||
|
||||
envlist = py35, report
|
||||
envlist = py35, py36, flake8
|
||||
|
||||
[travis]
|
||||
|
||||
python =
|
||||
3.5: py35, flake8
|
||||
3.6: py36, flake8
|
||||
|
||||
[testenv]
|
||||
|
||||
deps =
|
||||
aiohttp
|
||||
pytest
|
||||
# pytest-cov
|
||||
coverage
|
||||
|
||||
commands =
|
||||
coverage run -m pytest tests {posargs}
|
||||
mv .coverage .coverage.{envname}
|
||||
pytest tests {posargs}
|
||||
|
||||
basepython:
|
||||
py35: python3.5
|
||||
|
||||
whitelist_externals =
|
||||
coverage
|
||||
mv
|
||||
echo
|
||||
|
||||
[testenv:report]
|
||||
[testenv:flake8]
|
||||
deps =
|
||||
flake8
|
||||
|
||||
commands =
|
||||
coverage combine
|
||||
coverage report
|
||||
coverage html
|
||||
echo "Open file://{toxinidir}/coverage/index.html"
|
||||
|
||||
basepython =
|
||||
python3.5
|
||||
flake8 sanic
|
||||
|
|
Loading…
Reference in New Issue
Block a user