Merge branch 'master' into improved_config

This commit is contained in:
Eli Uriegas 2016-12-25 15:26:33 -08:00 committed by GitHub
commit 2d4512cd1c
21 changed files with 309 additions and 56 deletions

View File

@ -1,6 +1,7 @@
language: python language: python
python: python:
- '3.5' - '3.5'
- '3.6'
install: install:
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install -r requirements-dev.txt - pip install -r requirements-dev.txt

View File

@ -60,6 +60,7 @@ if __name__ == "__main__":
* [Cookies](docs/cookies.md) * [Cookies](docs/cookies.md)
* [Static Files](docs/static_files.md) * [Static Files](docs/static_files.md)
* [Configuration](docs/config.md) * [Configuration](docs/config.md)
* [Testing](docs/testing.md)
* [Deploying](docs/deploying.md) * [Deploying](docs/deploying.md)
* [Contributing](docs/contributing.md) * [Contributing](docs/contributing.md)
* [License](LICENSE) * [License](LICENSE)

View File

@ -6,6 +6,7 @@ Sanic has simple class based implementation. You should implement methods(get, p
```python ```python
from sanic import Sanic from sanic import Sanic
from sanic.views import HTTPMethodView from sanic.views import HTTPMethodView
from sanic.response import text
app = Sanic('some_name') app = Sanic('some_name')

View File

@ -27,3 +27,23 @@ async def handler(request):
app.run(host="0.0.0.0", port=8000) 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
View 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']
```

View 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)

View File

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

View File

@ -1,5 +1,3 @@
import logging import logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s")
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@ -1,9 +1,11 @@
from aiofiles import open as open_async from aiofiles import open as open_async
from .cookies import CookieJar
from mimetypes import guess_type from mimetypes import guess_type
from os import path from os import path
from ujson import dumps as json_dumps from ujson import dumps as json_dumps
from .cookies import CookieJar
COMMON_STATUS_CODES = { COMMON_STATUS_CODES = {
200: b'OK', 200: b'OK',
400: b'Bad Request', 400: b'Bad Request',
@ -79,7 +81,12 @@ class HTTPResponse:
self.content_type = content_type self.content_type = content_type
if body is not None: if body is not None:
self.body = body.encode('utf-8') 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: else:
self.body = body_bytes self.body = body_bytes

View File

@ -6,10 +6,11 @@ from multiprocessing import Process, Event
from signal import signal, SIGTERM, SIGINT from signal import signal, SIGTERM, SIGINT
from time import sleep from time import sleep
from traceback import format_exc from traceback import format_exc
import logging
from .config import Config from .config import Config
from .exceptions import Handler from .exceptions import Handler
from .log import log, logging from .log import log
from .response import HTTPResponse from .response import HTTPResponse
from .router import Router from .router import Router
from .server import serve from .server import serve
@ -18,7 +19,13 @@ from .exceptions import ServerError
class Sanic: 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: if name is None:
frame_records = stack()[1] frame_records = stack()[1]
name = getmodulename(frame_records[1]) name = getmodulename(frame_records[1])
@ -193,18 +200,18 @@ class Sanic:
if isawaitable(response): if isawaitable(response):
response = await response response = await response
# -------------------------------------------- # # -------------------------------------------- #
# Response Middleware # Response Middleware
# -------------------------------------------- # # -------------------------------------------- #
if self.response_middleware: if self.response_middleware:
for middleware in self.response_middleware: for middleware in self.response_middleware:
_response = middleware(request, response) _response = middleware(request, response)
if isawaitable(_response): if isawaitable(_response):
_response = await _response _response = await _response
if _response: if _response:
response = _response response = _response
break break
except Exception as e: except Exception as e:
# -------------------------------------------- # # -------------------------------------------- #

View File

@ -6,6 +6,7 @@ from signal import SIGINT, SIGTERM
from time import time from time import time
from httptools import HttpRequestParser from httptools import HttpRequestParser
from httptools.parser.errors import HttpParserError from httptools.parser.errors import HttpParserError
from .exceptions import ServerError
try: try:
import uvloop as async_loop import uvloop as async_loop
@ -14,7 +15,7 @@ except ImportError:
from .log import log from .log import log
from .request import Request from .request import Request
from .exceptions import RequestTimeout, PayloadTooLarge from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
class Signal: class Signal:
@ -105,9 +106,9 @@ class HttpProtocol(asyncio.Protocol):
# Parse request chunk or close connection # Parse request chunk or close connection
try: try:
self.parser.feed_data(data) self.parser.feed_data(data)
except HttpParserError as e: except HttpParserError:
self.bail_out( exception = InvalidUsage('Bad Request')
"Invalid request data, connection closed ({})".format(e)) self.write_error(exception)
def on_url(self, url): def on_url(self, url):
self.url = url self.url = url
@ -173,8 +174,9 @@ class HttpProtocol(asyncio.Protocol):
"Writing error failed, connection closed {}".format(e)) "Writing error failed, connection closed {}".format(e))
def bail_out(self, message): def bail_out(self, message):
log.debug(message) exception = ServerError(message)
self.transport.close() self.write_error(exception)
log.error(message)
def cleanup(self): def cleanup(self):
self.parser = None self.parser = None

View File

@ -2,6 +2,7 @@ from aiofiles.os import stat
from os import path from os import path
from re import sub from re import sub
from time import strftime, gmtime from time import strftime, gmtime
from urllib.parse import unquote
from .exceptions import FileNotFound, InvalidUsage from .exceptions import FileNotFound, InvalidUsage
from .response import file, HTTPResponse 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 # served. os.path.realpath seems to be very slow
if file_uri and '../' in file_uri: if file_uri and '../' in file_uri:
raise InvalidUsage("Invalid URL") raise InvalidUsage("Invalid URL")
# Merge served directory and requested file if provided # Merge served directory and requested file if provided
# Strip all / that in the beginning of the URL to help prevent python # 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 # from herping a derp and treating the uri as an absolute path
file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ file_path = file_or_directory
if file_uri else 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: try:
headers = {} headers = {}
# Check if the client has been sent this file before # Check if the client has been sent this file before

View File

@ -1,16 +1,30 @@
package main package main
import ( import (
"fmt" "encoding/json"
"os" "net/http"
"net/http" "os"
) )
type TestJSONResponse struct {
Test bool
}
func handler(w http.ResponseWriter, r *http.Request) { 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() { func main() {
http.HandleFunc("/", handler) http.HandleFunc("/", handler)
http.ListenAndServe(":" + os.Args[1], nil) http.ListenAndServe(":"+os.Args[1], nil)
} }

View File

@ -0,0 +1 @@
I need to be decoded as a uri

1
tests/static/test.file Normal file
View File

@ -0,0 +1 @@
I am just a regular static file

20
tests/test_bad_request.py Normal file
View File

@ -0,0 +1,20 @@
import asyncio
from sanic import Sanic
def test_bad_request_response():
app = Sanic('test_bad_request_response')
lines = []
async def _request(sanic, loop):
connect = asyncio.open_connection('127.0.0.1', 42101)
reader, writer = await connect
writer.write(b'not http')
while True:
line = await reader.readline()
if not line:
break
lines.append(line)
app.stop()
app.run(host='127.0.0.1', port=42101, debug=False, after_start=_request)
assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n'
assert lines[-1] == b'Error: Bad Request'

33
tests/test_logging.py Normal file
View 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()

View File

@ -2,6 +2,7 @@ from json import loads as json_loads, dumps as json_dumps
from sanic import Sanic from sanic import Sanic
from sanic.response import json, text from sanic.response import json, text
from sanic.utils import sanic_endpoint_test from sanic.utils import sanic_endpoint_test
from sanic.exceptions import ServerError
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
@ -32,6 +33,22 @@ def test_text():
assert response.text == 'Hello' 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(): def test_json():
app = Sanic('test_json') app = Sanic('test_json')

18
tests/test_response.py Normal file
View 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)

View File

@ -1,30 +1,62 @@
import inspect import inspect
import os import os
import pytest
from sanic import Sanic from sanic import Sanic
from sanic.utils import sanic_endpoint_test 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 = 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') request, response = sanic_endpoint_test(app, uri='/testing.file')
assert response.status == 200 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()) def test_static_directory(
current_directory = os.path.dirname(os.path.abspath(current_file)) static_file_directory, static_file_path, static_file_content):
with open(current_file, 'rb') as file:
current_file_contents = file.read()
app = Sanic('test_static') 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.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

18
tox.ini
View File

@ -1,27 +1,30 @@
[tox] [tox]
envlist = py35, report envlist = py35, py36
[testenv] [testenv]
deps = deps =
aiohttp aiohttp
pytest pytest
# pytest-cov
coverage coverage
commands = commands =
coverage run -m pytest tests {posargs} coverage run -m pytest -v tests {posargs}
mv .coverage .coverage.{envname} mv .coverage .coverage.{envname}
basepython:
py35: python3.5
whitelist_externals = whitelist_externals =
coverage coverage
mv mv
echo echo
[testenv:flake8]
deps =
flake8
commands =
flake8 sanic
[testenv:report] [testenv:report]
commands = commands =
@ -29,6 +32,3 @@ commands =
coverage report coverage report
coverage html coverage html
echo "Open file://{toxinidir}/coverage/index.html" echo "Open file://{toxinidir}/coverage/index.html"
basepython =
python3.5