Merge branch 'master' into ssl

This commit is contained in:
Raphael Deem 2017-01-17 18:09:33 -08:00 committed by GitHub
commit 9102a9cd6e
17 changed files with 309 additions and 31 deletions

View File

@ -74,6 +74,7 @@ app.run(host="0.0.0.0", port=8443, ssl=context)
* [Custom Protocol](docs/custom_protocol.md)
* [Testing](docs/testing.md)
* [Deploying](docs/deploying.md)
* [Extensions](docs/extensions.md)
* [Contributing](docs/contributing.md)
* [License](LICENSE)

6
docs/extensions.md Normal file
View File

@ -0,0 +1,6 @@
# Sanic Extensions
A list of Sanic extensions created by the community.
* [Sessions](https://github.com/subyraman/sanic_session) — Support for sessions. Allows using redis, memcache or an in memory store.
* [CORS](https://github.com/ashleysommer/sanic-cors) — A port of flask-cors.

View File

@ -9,6 +9,7 @@ The following request variables are accessible as properties:
`request.args` (dict) - Query String variables. Use getlist to get multiple of the same name
`request.form` (dict) - Posted form variables. Use getlist to get multiple of the same name
`request.body` (bytes) - Posted raw body. To get the raw data, regardless of content type
`request.ip` (str) - IP address of the requester
See request.py for more information

View File

@ -14,7 +14,7 @@ logging.basicConfig(
log = logging.getLogger()
# Set logger to override default basicConfig
sanic = Sanic(logger=True)
sanic = Sanic()
@sanic.route("/")
def test(request):
log.info("received request; responding with 'hey'")

View File

@ -12,3 +12,4 @@ kyoukai
falcon
tornado
aiofiles
beautifulsoup4

View File

@ -2,4 +2,3 @@ httptools
ujson
uvloop
aiofiles
multidict

View File

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

View File

@ -1,6 +1,104 @@
from .response import text
from .response import text, html
from .log import log
from traceback import format_exc
from traceback import format_exc, extract_tb
import sys
TRACEBACK_STYLE = '''
<style>
body {
padding: 20px;
font-family: Arial, sans-serif;
}
p {
margin: 0;
}
.summary {
padding: 10px;
}
h1 {
margin-bottom: 0;
}
h3 {
margin-top: 10px;
}
h3 code {
font-size: 24px;
}
.frame-line > * {
padding: 5px 10px;
}
.frame-line {
margin-bottom: 5px;
}
.frame-code {
font-size: 16px;
padding-left: 30px;
}
.tb-wrapper {
border: 1px solid #f3f3f3;
}
.tb-header {
background-color: #f3f3f3;
padding: 5px 10px;
}
.frame-descriptor {
background-color: #e2eafb;
}
.frame-descriptor {
font-size: 14px;
}
</style>
'''
TRACEBACK_WRAPPER_HTML = '''
<html>
<head>
{style}
</head>
<body>
<h1>{exc_name}</h1>
<h3><code>{exc_value}</code></h3>
<div class="tb-wrapper">
<p class="tb-header">Traceback (most recent call last):</p>
{frame_html}
<p class="summary">
<b>{exc_name}: {exc_value}</b>
while handling uri <code>{uri}</code>
</p>
</div>
</body>
</html>
'''
TRACEBACK_LINE_HTML = '''
<div class="frame-line">
<p class="frame-descriptor">
File {0.filename}, line <i>{0.lineno}</i>,
in <code><b>{0.name}</b></code>
</p>
<p class="frame-code"><code>{0.line}</code></p>
</div>
'''
INTERNAL_SERVER_ERROR_HTML = '''
<h1>Internal Server Error</h1>
<p>
The server encountered an internal error and cannot complete
your request.
</p>
'''
class SanicException(Exception):
@ -46,6 +144,21 @@ class Handler:
self.handlers = {}
self.sanic = sanic
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
@ -77,11 +190,12 @@ class Handler:
'Error: {}'.format(exception),
status=getattr(exception, 'status_code', 500))
elif self.sanic.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 text(response_message, status=500)
return html(html_output, status=500)
else:
return text(
'An error occurred while generating the response', status=500)
return html(INTERNAL_SERVER_ERROR_HTML, status=500)

View File

@ -41,18 +41,20 @@ class Request(dict):
Properties of an HTTP request such as URL, headers, etc.
"""
__slots__ = (
'url', 'headers', 'version', 'method', '_cookies',
'url', 'headers', 'version', 'method', '_cookies', 'transport',
'query_string', 'body',
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
'_ip',
)
def __init__(self, url_bytes, headers, version, method):
def __init__(self, url_bytes, headers, version, method, transport):
# TODO: Content-Encoding detection
url_parsed = parse_url(url_bytes)
self.url = url_parsed.path.decode('utf-8')
self.headers = headers
self.version = version
self.method = method
self.transport = transport
self.query_string = None
if url_parsed.query:
self.query_string = url_parsed.query.decode('utf-8')
@ -139,6 +141,12 @@ class Request(dict):
self._cookies = {}
return self._cookies
@property
def ip(self):
if not hasattr(self, '_ip'):
self._ip = self.transport.get_extra_info('peername')
return self._ip
File = namedtuple('File', ['type', 'body', 'name'])

View File

@ -83,10 +83,10 @@ class HTTPResponse:
if body is not None:
try:
# Try to encode it regularly
self.body = body.encode('utf-8')
self.body = body.encode()
except AttributeError:
# Convert it to a str if you can't
self.body = str(body).encode('utf-8')
self.body = str(body).encode()
else:
self.body = body_bytes
@ -169,3 +169,26 @@ async def file(location, mime_type=None, headers=None):
headers=headers,
content_type=mime_type,
body_bytes=out_stream)
def redirect(to, headers=None, status=302,
content_type="text/html; charset=utf-8"):
"""
Aborts execution and causes 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
:returns: the redirecting Response
"""
headers = headers or {}
# According to RFC 7231, a relative URI is now permitted.
headers['Location'] = to
return HTTPResponse(
status=status,
headers=headers,
content_type=content_type)

View File

@ -21,12 +21,7 @@ from os import set_inheritable
class Sanic:
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"
)
error_handler=None):
if name is None:
frame_records = stack()[1]
name = getmodulename(frame_records[1])
@ -154,7 +149,8 @@ class Sanic:
def register_blueprint(self, *args, **kwargs):
# TODO: deprecate 1.0
log.warning("Use of register_blueprint will be deprecated in "
"version 1.0. Please use the blueprint method instead")
"version 1.0. Please use the blueprint method instead",
DeprecationWarning)
return self.blueprint(*args, **kwargs)
# -------------------------------------------------------------------- #
@ -245,6 +241,7 @@ class Sanic:
# -------------------------------------------------------------------- #
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
after_start=None, before_stop=None, after_stop=None, ssl=None,
sock=None, workers=1, loop=None, protocol=HttpProtocol,
backlog=100, stop_event=None):
@ -270,6 +267,10 @@ class Sanic:
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s: %(levelname)s: %(message)s"
)
self.error_handler.debug = True
self.debug = debug
self.loop = loop
@ -356,6 +357,12 @@ class Sanic:
:param stop_event: if provided, is used as a stop signal
:return:
"""
# In case this is called directly, we configure logging here too.
# This won't interfere with the same call from run()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s: %(levelname)s: %(message)s"
)
server_settings['reuse_port'] = True
# Create a stop event to be triggered by a signal

View File

@ -1,7 +1,6 @@
import asyncio
from functools import partial
from inspect import isawaitable
from multidict import CIMultiDict
from signal import SIGINT, SIGTERM
from time import time
from httptools import HttpRequestParser
@ -18,11 +17,30 @@ from .request import Request
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
current_time = None
class Signal:
stopped = False
current_time = None
class CIDict(dict):
"""
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
"""
def get(self, key, default=None):
return super().get(key.casefold(), default)
def __getitem__(self, key):
return super().__getitem__(key.casefold())
def __setitem__(self, key, value):
return super().__setitem__(key.casefold(), value)
def __contains__(self, key):
return super().__contains__(key.casefold())
class HttpProtocol(asyncio.Protocol):
@ -118,18 +136,15 @@ class HttpProtocol(asyncio.Protocol):
exception = PayloadTooLarge('Payload Too Large')
self.write_error(exception)
self.headers.append((name.decode(), value.decode('utf-8')))
self.headers.append((name.decode().casefold(), value.decode()))
def on_headers_complete(self):
remote_addr = self.transport.get_extra_info('peername')
if remote_addr:
self.headers.append(('Remote-Addr', '%s:%s' % remote_addr))
self.request = Request(
url_bytes=self.url,
headers=CIMultiDict(self.headers),
headers=CIDict(self.headers),
version=self.parser.get_http_version(),
method=self.parser.get_method().decode()
method=self.parser.get_method().decode(),
transport=self.transport
)
def on_body(self, body):

View File

@ -1,4 +1,5 @@
import pytest
from bs4 import BeautifulSoup
from sanic import Sanic
from sanic.response import text
@ -75,7 +76,13 @@ def test_handled_unhandled_exception(exception_app):
request, response = sanic_endpoint_test(
exception_app, uri='/divide_by_zero')
assert response.status == 500
assert response.body == b'An error occurred while generating the response'
soup = BeautifulSoup(response.body, 'html.parser')
assert soup.h1.text == 'Internal Server Error'
message = " ".join(soup.p.text.split())
assert message == (
"The server encountered an internal error and "
"cannot complete your request.")
def test_exception_in_exception_handler(exception_app):

View File

@ -2,6 +2,7 @@ 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')
@ -21,6 +22,12 @@ def handler_3(request):
raise NotFound("OK")
@exception_handler_app.route('/4')
def handler_4(request):
foo = bar
return text(foo)
@exception_handler_app.exception(NotFound, ServerError)
def handler_exception(request, exception):
return text("OK")
@ -47,3 +54,20 @@ def test_text_exception__handler():
exception_handler_app, uri='/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)
assert response.status == 500
soup = BeautifulSoup(response.body, 'html.parser')
html = str(soup)
assert 'response = handler(request, *args, **kwargs)' in html
assert 'handler_4' in html
assert 'foo = bar' in html
summary_text = " ".join(soup.select('.summary')[0].text.split())
assert (
"NameError: name 'bar' "
"is not defined while handling uri /4") == summary_text

View File

@ -19,7 +19,7 @@ def test_log():
stream=log_stream
)
log = logging.getLogger()
app = Sanic('test_logging', logger=True)
app = Sanic('test_logging')
@app.route('/')
def handler(request):
log.info('hello world')

View File

@ -1,9 +1,10 @@
from json import loads as json_loads, dumps as json_dumps
from sanic import Sanic
from sanic.response import json, text
from sanic.response import json, text, redirect
from sanic.utils import sanic_endpoint_test
from sanic.exceptions import ServerError
import pytest
# ------------------------------------------------------------ #
# GET
@ -188,3 +189,73 @@ def test_post_form_multipart_form_data():
request, response = sanic_endpoint_test(app, data=payload, headers=headers)
assert request.form.get('test') == 'OK'
@pytest.fixture
def redirect_app():
app = Sanic('test_redirection')
@app.route('/redirect_init')
async def redirect_init(request):
return redirect("/redirect_target")
@app.route('/redirect_init_with_301')
async def redirect_init_with_301(request):
return redirect("/redirect_target", status=301)
@app.route('/redirect_target')
async def redirect_target(request):
return text('OK')
return app
def test_redirect_default_302(redirect_app):
"""
We expect a 302 default status code and the headers to be set.
"""
request, response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init",
allow_redirects=False)
assert response.status == 302
assert response.headers["Location"] == "/redirect_target"
assert response.headers["Content-Type"] == 'text/html; charset=utf-8'
def test_redirect_headers_none(redirect_app):
request, response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init",
headers=None,
allow_redirects=False)
assert response.status == 302
assert response.headers["Location"] == "/redirect_target"
def test_redirect_with_301(redirect_app):
"""
Test redirection with a different status code.
"""
request, response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init_with_301",
allow_redirects=False)
assert response.status == 301
assert response.headers["Location"] == "/redirect_target"
def test_get_then_redirect_follow_redirect(redirect_app):
"""
With `allow_redirects` we expect a 200.
"""
response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init", gather_request=False,
allow_redirects=True)
assert response.status == 200
assert response.text == 'OK'

View File

@ -13,6 +13,7 @@ python =
deps =
aiohttp
pytest
beautifulsoup4
commands =
pytest tests {posargs}