`_ that we use to track those projects!
Hello World Example
-------------------
@@ -47,8 +47,8 @@ Documentation
.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
-.. |Build Status| image:: https://travis-ci.org/channelcat/sanic.svg?branch=master
- :target: https://travis-ci.org/channelcat/sanic
+.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
+ :target: https://travis-ci.org/huge-success/sanic
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
@@ -59,11 +59,11 @@ Documentation
Examples
--------
-`Non-Core examples `_. Examples of plugins and Sanic that are outside the scope of Sanic core.
+`Non-Core examples `_. Examples of plugins and Sanic that are outside the scope of Sanic core.
-`Extensions `_. Sanic extensions created by the community.
+`Extensions `_. Sanic extensions created by the community.
-`Projects `_. Sanic in production use.
+`Projects `_. Sanic in production use.
TODO
diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md
index 01c89f95..c0728627 100644
--- a/docs/sanic/extensions.md
+++ b/docs/sanic/extensions.md
@@ -31,3 +31,4 @@ A list of Sanic extensions created by the community.
- [Sanic-Auth](https://github.com/pyx/sanic-auth): A minimal backend agnostic session-based user authentication mechanism for Sanic.
- [Sanic-CookieSession](https://github.com/pyx/sanic-cookiesession): A client-side only, cookie-based session, similar to the built-in session in Flask.
- [Sanic-WTF](https://github.com/pyx/sanic-wtf): Sanic-WTF makes using WTForms with Sanic and CSRF (Cross-Site Request Forgery) protection a little bit easier.
+- [sanic-sse](https://github.com/inn0kenty/sanic_sse): [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) implementation for Sanic.
diff --git a/docs/sanic/streaming.md b/docs/sanic/streaming.md
index a785322a..bf3ca664 100644
--- a/docs/sanic/streaming.md
+++ b/docs/sanic/streaming.md
@@ -37,7 +37,7 @@ async def handler(request):
if body is None:
break
body = body.decode('utf-8').replace('1', 'A')
- response.write(body)
+ await response.write(body)
return stream(streaming)
@@ -85,8 +85,8 @@ app = Sanic(__name__)
@app.route("/")
async def test(request):
async def sample_streaming_fn(response):
- response.write('foo,')
- response.write('bar')
+ await response.write('foo,')
+ await response.write('bar')
return stream(sample_streaming_fn, content_type='text/csv')
```
@@ -100,7 +100,7 @@ async def index(request):
conn = await asyncpg.connect(database='test')
async with conn.transaction():
async for record in conn.cursor('SELECT generate_series(0, 10)'):
- response.write(record[0])
+ await response.write(record[0])
return stream(stream_from_db)
```
diff --git a/examples/request_stream/server.py b/examples/request_stream/server.py
index e53a224c..d3d35aef 100644
--- a/examples/request_stream/server.py
+++ b/examples/request_stream/server.py
@@ -30,7 +30,7 @@ async def handler(request):
if body is None:
break
body = body.decode('utf-8').replace('1', 'A')
- response.write(body)
+ await response.write(body)
return stream(streaming)
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 3d94c51d..004f6f9e 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,5 +1,5 @@
aiofiles
-aiohttp>=2.3.0
+aiohttp>=2.3.0,<=3.2.1
chardet<=2.3.0
beautifulsoup4
coverage
diff --git a/sanic/__init__.py b/sanic/__init__.py
index 78bc7bd9..51c8268e 100644
--- a/sanic/__init__.py
+++ b/sanic/__init__.py
@@ -1,6 +1,6 @@
from sanic.app import Sanic
from sanic.blueprints import Blueprint
-__version__ = '0.7.0'
+__version__ = '0.8.3'
__all__ = ['Sanic', 'Blueprint']
diff --git a/sanic/app.py b/sanic/app.py
index 7af82111..acdf70c8 100644
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -386,13 +386,14 @@ class Sanic:
def static(self, uri, file_or_directory, pattern=r'/?.+',
use_modified_since=True, use_content_range=False,
stream_large_files=False, name='static', host=None,
- strict_slashes=None):
+ strict_slashes=None, content_type=None):
"""Register a root to serve files from. The input can either be a
file or a directory. See
"""
static_register(self, uri, file_or_directory, pattern,
use_modified_since, use_content_range,
- stream_large_files, name, host, strict_slashes)
+ stream_large_files, name, host, strict_slashes,
+ content_type)
def blueprint(self, blueprint, **options):
"""Register a blueprint on the application.
@@ -570,6 +571,10 @@ class Sanic:
:return: Nothing
"""
+ # Define `response` var here to remove warnings about
+ # allocation before assignment below.
+ response = None
+ cancelled = False
try:
# -------------------------------------------- #
# Request Middleware
@@ -596,6 +601,13 @@ class Sanic:
response = handler(request, *args, **kwargs)
if isawaitable(response):
response = await response
+ except CancelledError:
+ # If response handler times out, the server handles the error
+ # and cancels the handle_request job.
+ # In this case, the transport is already closed and we cannot
+ # issue a response.
+ response = None
+ cancelled = True
except Exception as e:
# -------------------------------------------- #
# Response Generation Failed
@@ -621,13 +633,22 @@ class Sanic:
# -------------------------------------------- #
# Response Middleware
# -------------------------------------------- #
- try:
- response = await self._run_response_middleware(request,
- response)
- except BaseException:
- error_logger.exception(
- 'Exception occurred in one of response middleware handlers'
- )
+ # Don't run response middleware if response is None
+ if response is not None:
+ try:
+ response = await self._run_response_middleware(request,
+ response)
+ except CancelledError:
+ # Response middleware can timeout too, as above.
+ response = None
+ cancelled = True
+ except BaseException:
+ error_logger.exception(
+ 'Exception occurred in one of response '
+ 'middleware handlers'
+ )
+ if cancelled:
+ raise CancelledError()
# pass the response to the correct callback
if isinstance(response, StreamingHTTPResponse):
@@ -670,8 +691,8 @@ class Sanic:
"""
# Default auto_reload to false
auto_reload = False
- # If debug is set, default it to true
- if debug:
+ # If debug is set, default it to true (unless on windows)
+ if debug and os.name == 'posix':
auto_reload = True
# Allow for overriding either of the defaults
auto_reload = kwargs.get("auto_reload", auto_reload)
@@ -687,11 +708,12 @@ class Sanic:
warnings.simplefilter('default')
warnings.warn("stop_event will be removed from future versions.",
DeprecationWarning)
+ # compatibility old access_log params
+ self.config.ACCESS_LOG = access_log
server_settings = self._helper(
host=host, port=port, debug=debug, ssl=ssl, sock=sock,
workers=workers, protocol=protocol, backlog=backlog,
- register_sys_signals=register_sys_signals,
- access_log=access_log, auto_reload=auto_reload)
+ register_sys_signals=register_sys_signals, auto_reload=auto_reload)
try:
self.is_running = True
@@ -745,12 +767,12 @@ class Sanic:
warnings.simplefilter('default')
warnings.warn("stop_event will be removed from future versions.",
DeprecationWarning)
-
+ # compatibility old access_log params
+ self.config.ACCESS_LOG = access_log
server_settings = self._helper(
host=host, port=port, debug=debug, ssl=ssl, sock=sock,
loop=get_event_loop(), protocol=protocol,
- backlog=backlog, run_async=True,
- access_log=access_log)
+ backlog=backlog, run_async=True)
# Trigger before_start events
await self.trigger_events(
@@ -795,8 +817,7 @@ class Sanic:
def _helper(self, host=None, port=None, debug=False,
ssl=None, sock=None, workers=1, loop=None,
protocol=HttpProtocol, backlog=100, stop_event=None,
- register_sys_signals=True, run_async=False, access_log=True,
- auto_reload=False):
+ register_sys_signals=True, run_async=False, auto_reload=False):
"""Helper function used by `run` and `create_server`."""
if isinstance(ssl, dict):
# try common aliaseses
@@ -837,7 +858,7 @@ class Sanic:
'loop': loop,
'register_sys_signals': register_sys_signals,
'backlog': backlog,
- 'access_log': access_log,
+ 'access_log': self.config.ACCESS_LOG,
'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE,
'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE,
'websocket_read_limit': self.config.WEBSOCKET_READ_LIMIT,
diff --git a/sanic/config.py b/sanic/config.py
index 8e1f383c..c5e42de5 100644
--- a/sanic/config.py
+++ b/sanic/config.py
@@ -39,6 +39,7 @@ class Config(dict):
self.WEBSOCKET_READ_LIMIT = 2 ** 16
self.WEBSOCKET_WRITE_LIMIT = 2 ** 16
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec
+ self.ACCESS_LOG = True
if load_env:
prefix = SANIC_PREFIX if load_env is True else load_env
diff --git a/sanic/reloader_helpers.py b/sanic/reloader_helpers.py
index 73759124..e0cb42e0 100644
--- a/sanic/reloader_helpers.py
+++ b/sanic/reloader_helpers.py
@@ -74,7 +74,14 @@ def kill_process_children_unix(pid):
with open(children_proc_path) as children_list_file_2:
children_list_pid_2 = children_list_file_2.read().split()
for _pid in children_list_pid_2:
- os.kill(int(_pid), signal.SIGTERM)
+ try:
+ os.kill(int(_pid), signal.SIGTERM)
+ except ProcessLookupError:
+ continue
+ try:
+ os.kill(int(child_pid), signal.SIGTERM)
+ except ProcessLookupError:
+ continue
def kill_process_children_osx(pid):
@@ -94,7 +101,7 @@ def kill_process_children(pid):
"""
if sys.platform == 'darwin':
kill_process_children_osx(pid)
- elif sys.platform == 'posix':
+ elif sys.platform == 'linux':
kill_process_children_unix(pid)
else:
pass # should signal error here
@@ -136,8 +143,8 @@ def watchdog(sleep_interval):
continue
elif mtime > old_time:
kill_process_children(worker_process.pid)
+ worker_process.terminate()
worker_process = restart_with_reloader()
-
mtimes[filename] = mtime
break
diff --git a/sanic/response.py b/sanic/response.py
index 2281766e..f169b4f2 100644
--- a/sanic/response.py
+++ b/sanic/response.py
@@ -46,7 +46,7 @@ class BaseHTTPResponse:
class StreamingHTTPResponse(BaseHTTPResponse):
__slots__ = (
- 'transport', 'streaming_fn', 'status',
+ 'protocol', 'streaming_fn', 'status',
'content_type', 'headers', '_cookies'
)
@@ -58,7 +58,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
self.headers = CIMultiDict(headers or {})
self._cookies = None
- def write(self, data):
+ async def write(self, data):
"""Writes a chunk of data to the streaming response.
:param data: bytes-ish data to be written.
@@ -66,8 +66,9 @@ class StreamingHTTPResponse(BaseHTTPResponse):
if type(data) != bytes:
data = self._encode_body(data)
- self.transport.write(
+ self.protocol.push_data(
b"%x\r\n%b\r\n" % (len(data), data))
+ await self.protocol.drain()
async def stream(
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
@@ -77,10 +78,12 @@ class StreamingHTTPResponse(BaseHTTPResponse):
headers = self.get_headers(
version, keep_alive=keep_alive,
keep_alive_timeout=keep_alive_timeout)
- self.transport.write(headers)
-
+ self.protocol.push_data(headers)
+ await self.protocol.drain()
await self.streaming_fn(self)
- self.transport.write(b'0\r\n\r\n')
+ self.protocol.push_data(b'0\r\n\r\n')
+ # no need to await drain here after this write, because it is the
+ # very last thing we write and nothing needs to wait for it.
def get_headers(
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
@@ -233,8 +236,8 @@ def html(body, status=200, headers=None):
content_type="text/html; charset=utf-8")
-async def file(
- location, mime_type=None, headers=None, filename=None, _range=None):
+async def file(location, status=200, mime_type=None, headers=None,
+ filename=None, _range=None):
"""Return a response object with file data.
:param location: Location of file on system.
@@ -260,15 +263,14 @@ async def file(
out_stream = await _file.read()
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
- return HTTPResponse(status=200,
+ return HTTPResponse(status=status,
headers=headers,
content_type=mime_type,
body_bytes=out_stream)
-async def file_stream(
- location, chunk_size=4096, mime_type=None, headers=None,
- filename=None, _range=None):
+async def file_stream(location, status=200, chunk_size=4096, mime_type=None,
+ headers=None, filename=None, _range=None):
"""Return a streaming response object with file data.
:param location: Location of file on system.
@@ -299,13 +301,13 @@ async def file_stream(
if len(content) < 1:
break
to_send -= len(content)
- response.write(content)
+ await response.write(content)
else:
while True:
content = await _file.read(chunk_size)
if len(content) < 1:
break
- response.write(content)
+ await response.write(content)
finally:
await _file.close()
return # Returning from this fn closes the stream
@@ -315,7 +317,7 @@ async def file_stream(
headers['Content-Range'] = 'bytes %s-%s/%s' % (
_range.start, _range.end, _range.total)
return StreamingHTTPResponse(streaming_fn=_streaming_fn,
- status=200,
+ status=status,
headers=headers,
content_type=mime_type)
diff --git a/sanic/server.py b/sanic/server.py
index 11e54edc..d5a8f211 100644
--- a/sanic/server.py
+++ b/sanic/server.py
@@ -55,7 +55,8 @@ class HttpProtocol(asyncio.Protocol):
# connection management
'_total_request_size', '_request_timeout_handler',
'_response_timeout_handler', '_keep_alive_timeout_handler',
- '_last_request_time', '_last_response_time', '_is_stream_handler')
+ '_last_request_time', '_last_response_time', '_is_stream_handler',
+ '_not_paused')
def __init__(self, *, loop, request_handler, error_handler,
signal=Signal(), connections=set(), request_timeout=60,
@@ -82,6 +83,7 @@ class HttpProtocol(asyncio.Protocol):
self.request_class = request_class or Request
self.is_request_stream = is_request_stream
self._is_stream_handler = False
+ self._not_paused = asyncio.Event(loop=loop)
self._total_request_size = 0
self._request_timeout_handler = None
self._response_timeout_handler = None
@@ -96,6 +98,7 @@ class HttpProtocol(asyncio.Protocol):
if 'requests_count' not in self.state:
self.state['requests_count'] = 0
self._debug = debug
+ self._not_paused.set()
@property
def keep_alive(self):
@@ -124,6 +127,12 @@ class HttpProtocol(asyncio.Protocol):
if self._keep_alive_timeout_handler:
self._keep_alive_timeout_handler.cancel()
+ def pause_writing(self):
+ self._not_paused.clear()
+
+ def resume_writing(self):
+ self._not_paused.set()
+
def request_timeout_callback(self):
# See the docstring in the RequestTimeout exception, to see
# exactly what this timeout is checking for.
@@ -351,6 +360,12 @@ class HttpProtocol(asyncio.Protocol):
self._last_response_time = current_time
self.cleanup()
+ async def drain(self):
+ await self._not_paused.wait()
+
+ def push_data(self, data):
+ self.transport.write(data)
+
async def stream_response(self, response):
"""
Streams a response to the client asynchronously. Attaches
@@ -360,9 +375,10 @@ class HttpProtocol(asyncio.Protocol):
if self._response_timeout_handler:
self._response_timeout_handler.cancel()
self._response_timeout_handler = None
+
try:
keep_alive = self.keep_alive
- response.transport = self.transport
+ response.protocol = self
await response.stream(
self.request.version, keep_alive, self.keep_alive_timeout)
self.log_response(response)
diff --git a/sanic/static.py b/sanic/static.py
index f2d02ab0..07831390 100644
--- a/sanic/static.py
+++ b/sanic/static.py
@@ -19,7 +19,7 @@ from sanic.response import file, file_stream, HTTPResponse
def register(app, uri, file_or_directory, pattern,
use_modified_since, use_content_range,
stream_large_files, name='static', host=None,
- strict_slashes=None):
+ strict_slashes=None, content_type=None):
# 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
@@ -41,6 +41,7 @@ def register(app, uri, file_or_directory, pattern,
If this is an integer, this represents the
threshold size to switch to file_stream()
:param name: user defined name used for url_for
+ :param content_type: user defined content type for header
"""
# If we're not trying to match a file directly,
# serve from the folder
@@ -95,10 +96,10 @@ def register(app, uri, file_or_directory, pattern,
del headers['Content-Length']
for key, value in _range.headers.items():
headers[key] = value
+ headers['Content-Type'] = content_type \
+ or guess_type(file_path)[0] or 'text/plain'
if request.method == 'HEAD':
- return HTTPResponse(
- headers=headers,
- content_type=guess_type(file_path)[0] or 'text/plain')
+ return HTTPResponse(headers=headers)
else:
if stream_large_files:
if isinstance(stream_large_files, int):
diff --git a/tests/static/test.html b/tests/static/test.html
new file mode 100644
index 00000000..4ba71873
--- /dev/null
+++ b/tests/static/test.html
@@ -0,0 +1,26 @@
+
+
+
+ ▄▄▄▄▄
+ ▀▀▀██████▄▄▄ _______________
+ ▄▄▄▄▄ █████████▄ / \
+ ▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
+ ▀▀█████▄▄ ▀██████▄██ | _________________/
+ ▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
+ ▀▀▀▄ ▀▀███ ▀ ▄▄
+ ▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
+ ██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
+▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
+▌ ▐▀████▐███▒▒▒▒▒▐██▌
+▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
+ ▀▀█████████▀
+ ▄▄██▀██████▀█
+ ▄██▀ ▀▀▀ █
+ ▄█ ▐▌
+ ▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
+▌ ▐ ▀▀▄▄▄▀
+ ▀▀▄▄▀
+
+
+
+
diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py
index 4c321646..37756085 100644
--- a/tests/test_blueprints.py
+++ b/tests/test_blueprints.py
@@ -1,5 +1,6 @@
import asyncio
import inspect
+import os
import pytest
from sanic import Sanic
@@ -13,6 +14,14 @@ from sanic.constants import HTTP_METHODS
# GET
# ------------------------------------------------------------ #
+def get_file_path(static_file_directory, file_name):
+ return os.path.join(static_file_directory, file_name)
+
+def get_file_content(static_file_directory, file_name):
+ """The content of the static file to check"""
+ with open(get_file_path(static_file_directory, file_name), 'rb') as file:
+ return file.read()
+
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_versioned_routes_get(method):
app = Sanic('test_shorhand_routes_get')
@@ -348,6 +357,28 @@ def test_bp_static():
assert response.status == 200
assert response.body == current_file_contents
+@pytest.mark.parametrize('file_name', ['test.html'])
+def test_bp_static_content_type(file_name):
+ # This is done here, since no other test loads a file here
+ current_file = inspect.getfile(inspect.currentframe())
+ current_directory = os.path.dirname(os.path.abspath(current_file))
+ static_directory = os.path.join(current_directory, 'static')
+
+ app = Sanic('test_static')
+ blueprint = Blueprint('test_static')
+ blueprint.static(
+ '/testing.file',
+ get_file_path(static_directory, file_name),
+ content_type='text/html; charset=utf-8'
+ )
+
+ app.blueprint(blueprint)
+
+ request, response = app.test_client.get('/testing.file')
+ assert response.status == 200
+ assert response.body == get_file_content(static_directory, file_name)
+ assert response.headers['Content-Type'] == 'text/html; charset=utf-8'
+
def test_bp_shorthand():
app = Sanic('test_shorhand_routes')
blueprint = Blueprint('test_shorhand_routes')
@@ -449,41 +480,41 @@ def test_bp_shorthand():
def test_bp_group():
app = Sanic('test_nested_bp_groups')
-
+
deep_0 = Blueprint('deep_0', url_prefix='/deep')
deep_1 = Blueprint('deep_1', url_prefix = '/deep1')
@deep_0.route('/')
def handler(request):
return text('D0_OK')
-
+
@deep_1.route('/bottom')
def handler(request):
return text('D1B_OK')
mid_0 = Blueprint.group(deep_0, deep_1, url_prefix='/mid')
mid_1 = Blueprint('mid_tier', url_prefix='/mid1')
-
+
@mid_1.route('/')
def handler(request):
return text('M1_OK')
top = Blueprint.group(mid_0, mid_1)
-
+
app.blueprint(top)
-
+
@app.route('/')
def handler(request):
return text('TOP_OK')
-
+
request, response = app.test_client.get('/')
assert response.text == 'TOP_OK'
-
+
request, response = app.test_client.get('/mid1')
assert response.text == 'M1_OK'
-
+
request, response = app.test_client.get('/mid/deep')
assert response.text == 'D0_OK'
-
+
request, response = app.test_client.get('/mid/deep1/bottom')
assert response.text == 'D1B_OK'
diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py
index 2a9e93a2..53a2872e 100644
--- a/tests/test_keep_alive_timeout.py
+++ b/tests/test_keep_alive_timeout.py
@@ -9,14 +9,39 @@ import aiohttp
from aiohttp import TCPConnector
from sanic.testing import SanicTestClient, HOST, PORT
+try:
+ try:
+ import packaging # direct use
+ except ImportError:
+ # setuptools v39.0 and above.
+ try:
+ from setuptools.extern import packaging
+ except ImportError:
+ # Before setuptools v39.0
+ from pkg_resources.extern import packaging
+ version = packaging.version
+except ImportError:
+ raise RuntimeError("The 'packaging' library is missing.")
+
+aiohttp_version = version.parse(aiohttp.__version__)
class ReuseableTCPConnector(TCPConnector):
def __init__(self, *args, **kwargs):
super(ReuseableTCPConnector, self).__init__(*args, **kwargs)
self.old_proto = None
- if aiohttp.__version__ >= '3.0':
-
+ if aiohttp_version >= version.parse('3.3.0'):
+ async def connect(self, req, traces, timeout):
+ new_conn = await super(ReuseableTCPConnector, self)\
+ .connect(req, traces, timeout)
+ if self.old_proto is not None:
+ if self.old_proto != new_conn._protocol:
+ raise RuntimeError(
+ "We got a new connection, wanted the same one!")
+ print(new_conn.__dict__)
+ self.old_proto = new_conn._protocol
+ return new_conn
+ elif aiohttp_version >= version.parse('3.0.0'):
async def connect(self, req, traces=None):
new_conn = await super(ReuseableTCPConnector, self)\
.connect(req, traces=traces)
@@ -28,7 +53,6 @@ class ReuseableTCPConnector(TCPConnector):
self.old_proto = new_conn._protocol
return new_conn
else:
-
async def connect(self, req):
new_conn = await super(ReuseableTCPConnector, self)\
.connect(req)
diff --git a/tests/test_request_stream.py b/tests/test_request_stream.py
index 4ca4e44e..b14aa519 100644
--- a/tests/test_request_stream.py
+++ b/tests/test_request_stream.py
@@ -83,7 +83,7 @@ def test_request_stream_app():
body = await request.stream.get()
if body is None:
break
- response.write(body.decode('utf-8'))
+ await response.write(body.decode('utf-8'))
return stream(streaming)
@app.put('/_put')
@@ -100,7 +100,7 @@ def test_request_stream_app():
body = await request.stream.get()
if body is None:
break
- response.write(body.decode('utf-8'))
+ await response.write(body.decode('utf-8'))
return stream(streaming)
@app.patch('/_patch')
@@ -117,7 +117,7 @@ def test_request_stream_app():
body = await request.stream.get()
if body is None:
break
- response.write(body.decode('utf-8'))
+ await response.write(body.decode('utf-8'))
return stream(streaming)
assert app.is_request_stream is True
@@ -177,7 +177,7 @@ def test_request_stream_handle_exception():
body = await request.stream.get()
if body is None:
break
- response.write(body.decode('utf-8'))
+ await response.write(body.decode('utf-8'))
return stream(streaming)
# 404
@@ -231,7 +231,7 @@ def test_request_stream_blueprint():
body = await request.stream.get()
if body is None:
break
- response.write(body.decode('utf-8'))
+ await response.write(body.decode('utf-8'))
return stream(streaming)
@bp.put('/_put')
@@ -248,7 +248,7 @@ def test_request_stream_blueprint():
body = await request.stream.get()
if body is None:
break
- response.write(body.decode('utf-8'))
+ await response.write(body.decode('utf-8'))
return stream(streaming)
@bp.patch('/_patch')
@@ -265,7 +265,7 @@ def test_request_stream_blueprint():
body = await request.stream.get()
if body is None:
break
- response.write(body.decode('utf-8'))
+ await response.write(body.decode('utf-8'))
return stream(streaming)
app.blueprint(bp)
@@ -380,7 +380,7 @@ def test_request_stream():
body = await request.stream.get()
if body is None:
break
- response.write(body.decode('utf-8'))
+ await response.write(body.decode('utf-8'))
return stream(streaming)
@app.get('/get')
diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py
index b3eb78aa..672d0588 100644
--- a/tests/test_request_timeout.py
+++ b/tests/test_request_timeout.py
@@ -5,9 +5,24 @@ import asyncio
from sanic.response import text
from sanic.config import Config
import aiohttp
-from aiohttp import TCPConnector
+from aiohttp import TCPConnector, ClientResponse
from sanic.testing import SanicTestClient, HOST, PORT
+try:
+ try:
+ import packaging # direct use
+ except ImportError:
+ # setuptools v39.0 and above.
+ try:
+ from setuptools.extern import packaging
+ except ImportError:
+ # Before setuptools v39.0
+ from pkg_resources.extern import packaging
+ version = packaging.version
+except ImportError:
+ raise RuntimeError("The 'packaging' library is missing.")
+
+aiohttp_version = version.parse(aiohttp.__version__)
class DelayableTCPConnector(TCPConnector):
@@ -38,8 +53,11 @@ class DelayableTCPConnector(TCPConnector):
self.orig_start = getattr(resp, 'start')
try:
- ret = await self.orig_start(connection,
- read_until_eof)
+ if aiohttp_version >= version.parse("3.3.0"):
+ ret = await self.orig_start(connection)
+ else:
+ ret = await self.orig_start(connection,
+ read_until_eof)
except Exception as e:
raise e
return ret
@@ -57,15 +75,31 @@ class DelayableTCPConnector(TCPConnector):
await asyncio.sleep(self.delay)
t = req.loop.time()
print("sending at {}".format(t), flush=True)
- conn = next(iter(args)) # first arg is connection
- if aiohttp.__version__ >= "3.1.0":
+ conn = next(iter(args)) # first arg is connection
+
+ if aiohttp_version >= version.parse("3.1.0"):
try:
delayed_resp = await self.orig_send(*args, **kwargs)
except Exception as e:
- return aiohttp.ClientResponse(req.method, req.url,
- writer=None, continue100=None, timer=None,
- request_info=None, auto_decompress=None, traces=[],
- loop=req.loop, session=None)
+ if aiohttp_version >= version.parse("3.3.0"):
+ return aiohttp.ClientResponse(req.method, req.url,
+ writer=None,
+ continue100=None,
+ timer=None,
+ request_info=None,
+ traces=[],
+ loop=req.loop,
+ session=None)
+ else:
+ return aiohttp.ClientResponse(req.method, req.url,
+ writer=None,
+ continue100=None,
+ timer=None,
+ request_info=None,
+ auto_decompress=None,
+ traces=[],
+ loop=req.loop,
+ session=None)
else:
try:
delayed_resp = self.orig_send(*args, **kwargs)
@@ -73,7 +107,7 @@ class DelayableTCPConnector(TCPConnector):
return aiohttp.ClientResponse(req.method, req.url)
return delayed_resp
- if aiohttp.__version__ >= "3.1.0":
+ if aiohttp_version >= version.parse("3.1.0"):
# aiohttp changed the request.send method to async
async def send(self, *args, **kwargs):
gen = self.delayed_send(*args, **kwargs)
@@ -96,12 +130,25 @@ class DelayableTCPConnector(TCPConnector):
self._post_connect_delay = _post_connect_delay
self._pre_request_delay = _pre_request_delay
- if aiohttp.__version__ >= '3.0':
-
+ if aiohttp_version >= version.parse("3.3.0"):
+ async def connect(self, req, traces, timeout):
+ d_req = DelayableTCPConnector.\
+ RequestContextManager(req, self._pre_request_delay)
+ conn = await super(DelayableTCPConnector, self).\
+ connect(req, traces, timeout)
+ if self._post_connect_delay and self._post_connect_delay > 0:
+ await asyncio.sleep(self._post_connect_delay,
+ loop=self._loop)
+ req.send = d_req.send
+ t = req.loop.time()
+ print("Connected at {}".format(t), flush=True)
+ return conn
+ elif aiohttp_version >= version.parse("3.0.0"):
async def connect(self, req, traces=None):
d_req = DelayableTCPConnector.\
RequestContextManager(req, self._pre_request_delay)
- conn = await super(DelayableTCPConnector, self).connect(req, traces=traces)
+ conn = await super(DelayableTCPConnector, self).\
+ connect(req, traces=traces)
if self._post_connect_delay and self._post_connect_delay > 0:
await asyncio.sleep(self._post_connect_delay,
loop=self._loop)
diff --git a/tests/test_response.py b/tests/test_response.py
index 36259970..6dcd2ea6 100644
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -10,6 +10,7 @@ from random import choice
from sanic import Sanic
from sanic.response import HTTPResponse, stream, StreamingHTTPResponse, file, file_stream, json
+from sanic.server import HttpProtocol
from sanic.testing import HOST, PORT
from unittest.mock import MagicMock
@@ -30,9 +31,10 @@ def test_response_body_not_a_string():
async def sample_streaming_fn(response):
- response.write('foo,')
+ await response.write('foo,')
await asyncio.sleep(.001)
- response.write('bar')
+ await response.write('bar')
+
def test_method_not_allowed():
@@ -189,20 +191,30 @@ def test_stream_response_includes_chunked_header():
def test_stream_response_writes_correct_content_to_transport(streaming_app):
response = StreamingHTTPResponse(sample_streaming_fn)
- response.transport = MagicMock(asyncio.Transport)
+ response.protocol = MagicMock(HttpProtocol)
+ response.protocol.transport = MagicMock(asyncio.Transport)
+
+ async def mock_drain():
+ pass
+
+ def mock_push_data(data):
+ response.protocol.transport.write(data)
+
+ response.protocol.push_data = mock_push_data
+ response.protocol.drain = mock_drain
@streaming_app.listener('after_server_start')
async def run_stream(app, loop):
await response.stream()
- assert response.transport.write.call_args_list[1][0][0] == (
+ assert response.protocol.transport.write.call_args_list[1][0][0] == (
b'4\r\nfoo,\r\n'
)
- assert response.transport.write.call_args_list[2][0][0] == (
+ assert response.protocol.transport.write.call_args_list[2][0][0] == (
b'3\r\nbar\r\n'
)
- assert response.transport.write.call_args_list[3][0][0] == (
+ assert response.protocol.transport.write.call_args_list[3][0][0] == (
b'0\r\n\r\n'
)
@@ -227,17 +239,19 @@ def get_file_content(static_file_directory, file_name):
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
-def test_file_response(file_name, static_file_directory):
+@pytest.mark.parametrize('status', [200, 401])
+def test_file_response(file_name, static_file_directory, status):
app = Sanic('test_file_helper')
@app.route('/files/', methods=['GET'])
def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
file_path = os.path.abspath(unquote(file_path))
- return file(file_path, mime_type=guess_type(file_path)[0] or 'text/plain')
+ return file(file_path, status=status,
+ mime_type=guess_type(file_path)[0] or 'text/plain')
request, response = app.test_client.get('/files/{}'.format(file_name))
- assert response.status == 200
+ assert response.status == status
assert response.body == get_file_content(static_file_directory, file_name)
assert 'Content-Disposition' not in response.headers
diff --git a/tests/test_response_timeout.py b/tests/test_response_timeout.py
index bf55a42e..44204b5e 100644
--- a/tests/test_response_timeout.py
+++ b/tests/test_response_timeout.py
@@ -7,6 +7,7 @@ from sanic.config import Config
Config.RESPONSE_TIMEOUT = 1
response_timeout_app = Sanic('test_response_timeout')
response_timeout_default_app = Sanic('test_response_timeout_default')
+response_handler_cancelled_app = Sanic('test_response_handler_cancelled')
@response_timeout_app.route('/1')
@@ -36,3 +37,29 @@ def test_default_server_error_response_timeout():
request, response = response_timeout_default_app.test_client.get('/1')
assert response.status == 503
assert response.text == 'Error: Response Timeout'
+
+
+response_handler_cancelled_app.flag = False
+
+
+@response_handler_cancelled_app.exception(asyncio.CancelledError)
+def handler_cancelled(request, exception):
+ # If we get a CancelledError, it means sanic has already sent a response,
+ # we should not ever have to handle a CancelledError.
+ response_handler_cancelled_app.flag = True
+ return text("App received CancelledError!", 500)
+ # The client will never receive this response, because the socket
+ # is already closed when we get a CancelledError.
+
+
+@response_handler_cancelled_app.route('/1')
+async def handler_3(request):
+ await asyncio.sleep(2)
+ return text('OK')
+
+
+def test_response_handler_cancelled():
+ request, response = response_handler_cancelled_app.test_client.get('/1')
+ assert response.status == 503
+ assert response.text == 'Error: Response Timeout'
+ assert response_handler_cancelled_app.flag is False
diff --git a/tests/test_static.py b/tests/test_static.py
index 276001cc..3335e248 100644
--- a/tests/test_static.py
+++ b/tests/test_static.py
@@ -36,6 +36,21 @@ def test_static_file(static_file_directory, file_name):
assert response.body == get_file_content(static_file_directory, file_name)
+@pytest.mark.parametrize('file_name', ['test.html'])
+def test_static_file_content_type(static_file_directory, file_name):
+ app = Sanic('test_static')
+ app.static(
+ '/testing.file',
+ get_file_path(static_file_directory, file_name),
+ content_type='text/html; charset=utf-8'
+ )
+
+ request, response = app.test_client.get('/testing.file')
+ assert response.status == 200
+ assert response.body == get_file_content(static_file_directory, file_name)
+ assert response.headers['Content-Type'] == 'text/html; charset=utf-8'
+
+
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
@pytest.mark.parametrize('base_uri', ['/static', '', '/dir'])
def test_static_directory(file_name, base_uri, static_file_directory):