Merge pull request #1 from huge-success/master

Merge from head
This commit is contained in:
Stephen Sadowski 2018-09-14 08:20:37 -05:00 committed by GitHub
commit f38783bdef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 333 additions and 100 deletions

View File

@ -31,7 +31,7 @@ deploy:
provider: pypi provider: pypi
user: channelcat user: channelcat
password: password:
secure: OgADRQH3+dTL5swGzXkeRJDNbLpFzwqYnXB4iLD0Npvzj9QnKyQVvkbaeq6VmV9dpEFb5ULaAKYQq19CrXYDm28yanUSn6jdJ4SukaHusi7xt07U6H7pmoX/uZ2WZYqCSLM8cSp8TXY/3oV3rY5Jfj/AibE5XTbim5/lrhsvW6NR+ALzxc0URRPAHDZEPpojTCjSTjpY0aDsaKWg4mXVRMFfY3O68j6KaIoukIZLuoHfePLKrbZxaPG5VxNhMHEaICdxVxE/dO+7pQmQxXuIsEOHK1QiVJ9YrSGcNqgEqhN36kYP8dqMeVB07sv8Xa6o/Uax2/wXS2HEJvuwP1YD6WkoZuo9ZB85bcMdg7BV9jJDbVFVPJwc75BnTLHrMa3Q1KrRlKRDBUXBUsQivPuWhFNwUgvEayq2qSI3aRQR4Z0O+DfboEhXYojSoD64/EWBTZ7vhgbvOTGEdukUQSYrKj9P8jc1s8exomTsAiqdFxTUpzfiammUSL+M93lP4urtahl1jjXFX7gd3DzdEEb0NsGkx5lm/qdsty8/TeAvKUmC+RVU6T856W6MqN0P+yGbpWUARcSE7fwztC3SPxwAuxvIN3BHmRhOUHoORPNG2VpfbnscIzBKJR4v0JKzbpi0IDa66K+tCGsCEvQuL4cxVOtoUySPWNSUAyUWWUrGM2k= secure: h7oNDjA/ObDBGK7xt55SV0INHOclMJW/byxMrNxvCZ0JxiRk7WBNtWYt0WJjyf5lO/L0/sfgiAk0GIdFon57S24njSLPAq/a4ptkWZ68s2A+TaF6ezJSZvE9V8khivjoeub90TzfX6P5aukRja1CSxXKJm+v0V8hGE4CZGyCgEDvK3JqIakpXllSDl19DhVftCS/lQZD7AXrZlg1kZnPCMtB5IbCVR4L2bfrSJVNptBi2CqqxacY2MOLu+jv5FzJ2BGVIJ2zoIJS2T+JmGJzpiamF6y8Amv0667i9lg2DXWCtI3PsQzCmwa3F/ZsI+ohUAvJC5yvzP7SyTJyXifRBdJ9O137QkNAHFoJOOY3B4GSnTo8/boajKXEqGiV4h2EgwNjBaR0WJl0pB7HHUCBMkNRWqo6ACB8eCr04tXWXPvkGIc+wPjq960hsUZea1O31MuktYc9Ot6eiFqm7OKoItdi7LxCen1eTj93ePgkiEnVZ+p/04Hh1U7CX31UJMNu5kCvZPIANnAuDsS2SK7Qkr88OAuWL0wmrBcXKOcnVkJtZ5mzx8T54bI1RrSYtFDBLFfOPb0GucSziMBtQpE76qPEauVwIXBk3RnR8N57xBR/lvTaIk758tf+haO0llEO5rVls1zLNZ+VlTzXy7hX0OZbdopIAcCFBFWqWMAdXQc=
on: on:
tags: true tags: true
distributions: "sdist bdist_wheel" distributions: "sdist bdist_wheel"

View File

@ -7,9 +7,9 @@ Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's ba
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy. On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome! Sanic is developed `on GitHub <https://github.com/huge-success/sanic/>`_. Contributions are welcome!
If you have a project that utilizes Sanic make sure to comment on the `issue <https://github.com/channelcat/sanic/issues/396>`_ that we use to track those projects! If you have a project that utilizes Sanic make sure to comment on the `issue <https://github.com/huge-success/sanic/issues/396>`_ that we use to track those projects!
Hello World Example 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 .. |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 :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 .. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
:target: https://travis-ci.org/channelcat/sanic :target: https://travis-ci.org/huge-success/sanic
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest .. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
:target: http://sanic.readthedocs.io/en/latest/?badge=latest :target: http://sanic.readthedocs.io/en/latest/?badge=latest
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg .. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
@ -59,11 +59,11 @@ Documentation
Examples Examples
-------- --------
`Non-Core examples <https://github.com/channelcat/sanic/wiki/Examples/>`_. Examples of plugins and Sanic that are outside the scope of Sanic core. `Non-Core examples <https://github.com/huge-success/sanic/wiki/Examples/>`_. Examples of plugins and Sanic that are outside the scope of Sanic core.
`Extensions <https://github.com/channelcat/sanic/wiki/Extensions/>`_. Sanic extensions created by the community. `Extensions <https://github.com/huge-success/sanic/wiki/Extensions/>`_. Sanic extensions created by the community.
`Projects <https://github.com/channelcat/sanic/wiki/Projects/>`_. Sanic in production use. `Projects <https://github.com/huge-success/sanic/wiki/Projects/>`_. Sanic in production use.
TODO TODO

View File

@ -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-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-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-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.

View File

@ -37,7 +37,7 @@ async def handler(request):
if body is None: if body is None:
break break
body = body.decode('utf-8').replace('1', 'A') body = body.decode('utf-8').replace('1', 'A')
response.write(body) await response.write(body)
return stream(streaming) return stream(streaming)
@ -85,8 +85,8 @@ app = Sanic(__name__)
@app.route("/") @app.route("/")
async def test(request): async def test(request):
async def sample_streaming_fn(response): async def sample_streaming_fn(response):
response.write('foo,') await response.write('foo,')
response.write('bar') await response.write('bar')
return stream(sample_streaming_fn, content_type='text/csv') return stream(sample_streaming_fn, content_type='text/csv')
``` ```
@ -100,7 +100,7 @@ async def index(request):
conn = await asyncpg.connect(database='test') conn = await asyncpg.connect(database='test')
async with conn.transaction(): async with conn.transaction():
async for record in conn.cursor('SELECT generate_series(0, 10)'): 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) return stream(stream_from_db)
``` ```

View File

@ -30,7 +30,7 @@ async def handler(request):
if body is None: if body is None:
break break
body = body.decode('utf-8').replace('1', 'A') body = body.decode('utf-8').replace('1', 'A')
response.write(body) await response.write(body)
return stream(streaming) return stream(streaming)

View File

@ -1,5 +1,5 @@
aiofiles aiofiles
aiohttp>=2.3.0 aiohttp>=2.3.0,<=3.2.1
chardet<=2.3.0 chardet<=2.3.0
beautifulsoup4 beautifulsoup4
coverage coverage

View File

@ -1,6 +1,6 @@
from sanic.app import Sanic from sanic.app import Sanic
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
__version__ = '0.7.0' __version__ = '0.8.3'
__all__ = ['Sanic', 'Blueprint'] __all__ = ['Sanic', 'Blueprint']

View File

@ -386,13 +386,14 @@ class Sanic:
def static(self, uri, file_or_directory, pattern=r'/?.+', def static(self, uri, file_or_directory, pattern=r'/?.+',
use_modified_since=True, use_content_range=False, use_modified_since=True, use_content_range=False,
stream_large_files=False, name='static', host=None, 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 """Register a root to serve files from. The input can either be a
file or a directory. See file or a directory. See
""" """
static_register(self, uri, file_or_directory, pattern, static_register(self, uri, file_or_directory, pattern,
use_modified_since, use_content_range, 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): def blueprint(self, blueprint, **options):
"""Register a blueprint on the application. """Register a blueprint on the application.
@ -570,6 +571,10 @@ class Sanic:
:return: Nothing :return: Nothing
""" """
# Define `response` var here to remove warnings about
# allocation before assignment below.
response = None
cancelled = False
try: try:
# -------------------------------------------- # # -------------------------------------------- #
# Request Middleware # Request Middleware
@ -596,6 +601,13 @@ class Sanic:
response = handler(request, *args, **kwargs) response = handler(request, *args, **kwargs)
if isawaitable(response): if isawaitable(response):
response = await 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: except Exception as e:
# -------------------------------------------- # # -------------------------------------------- #
# Response Generation Failed # Response Generation Failed
@ -621,13 +633,22 @@ class Sanic:
# -------------------------------------------- # # -------------------------------------------- #
# Response Middleware # Response Middleware
# -------------------------------------------- # # -------------------------------------------- #
try: # Don't run response middleware if response is None
response = await self._run_response_middleware(request, if response is not None:
response) try:
except BaseException: response = await self._run_response_middleware(request,
error_logger.exception( response)
'Exception occurred in one of response middleware handlers' 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 # pass the response to the correct callback
if isinstance(response, StreamingHTTPResponse): if isinstance(response, StreamingHTTPResponse):
@ -670,8 +691,8 @@ class Sanic:
""" """
# Default auto_reload to false # Default auto_reload to false
auto_reload = False auto_reload = False
# If debug is set, default it to true # If debug is set, default it to true (unless on windows)
if debug: if debug and os.name == 'posix':
auto_reload = True auto_reload = True
# Allow for overriding either of the defaults # Allow for overriding either of the defaults
auto_reload = kwargs.get("auto_reload", auto_reload) auto_reload = kwargs.get("auto_reload", auto_reload)
@ -687,11 +708,12 @@ class Sanic:
warnings.simplefilter('default') warnings.simplefilter('default')
warnings.warn("stop_event will be removed from future versions.", warnings.warn("stop_event will be removed from future versions.",
DeprecationWarning) DeprecationWarning)
# compatibility old access_log params
self.config.ACCESS_LOG = access_log
server_settings = self._helper( server_settings = self._helper(
host=host, port=port, debug=debug, ssl=ssl, sock=sock, host=host, port=port, debug=debug, ssl=ssl, sock=sock,
workers=workers, protocol=protocol, backlog=backlog, workers=workers, protocol=protocol, backlog=backlog,
register_sys_signals=register_sys_signals, register_sys_signals=register_sys_signals, auto_reload=auto_reload)
access_log=access_log, auto_reload=auto_reload)
try: try:
self.is_running = True self.is_running = True
@ -745,12 +767,12 @@ class Sanic:
warnings.simplefilter('default') warnings.simplefilter('default')
warnings.warn("stop_event will be removed from future versions.", warnings.warn("stop_event will be removed from future versions.",
DeprecationWarning) DeprecationWarning)
# compatibility old access_log params
self.config.ACCESS_LOG = access_log
server_settings = self._helper( server_settings = self._helper(
host=host, port=port, debug=debug, ssl=ssl, sock=sock, host=host, port=port, debug=debug, ssl=ssl, sock=sock,
loop=get_event_loop(), protocol=protocol, loop=get_event_loop(), protocol=protocol,
backlog=backlog, run_async=True, backlog=backlog, run_async=True)
access_log=access_log)
# Trigger before_start events # Trigger before_start events
await self.trigger_events( await self.trigger_events(
@ -795,8 +817,7 @@ class Sanic:
def _helper(self, host=None, port=None, debug=False, def _helper(self, host=None, port=None, debug=False,
ssl=None, sock=None, workers=1, loop=None, ssl=None, sock=None, workers=1, loop=None,
protocol=HttpProtocol, backlog=100, stop_event=None, protocol=HttpProtocol, backlog=100, stop_event=None,
register_sys_signals=True, run_async=False, access_log=True, register_sys_signals=True, run_async=False, auto_reload=False):
auto_reload=False):
"""Helper function used by `run` and `create_server`.""" """Helper function used by `run` and `create_server`."""
if isinstance(ssl, dict): if isinstance(ssl, dict):
# try common aliaseses # try common aliaseses
@ -837,7 +858,7 @@ class Sanic:
'loop': loop, 'loop': loop,
'register_sys_signals': register_sys_signals, 'register_sys_signals': register_sys_signals,
'backlog': backlog, 'backlog': backlog,
'access_log': access_log, 'access_log': self.config.ACCESS_LOG,
'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE, 'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE,
'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE, 'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE,
'websocket_read_limit': self.config.WEBSOCKET_READ_LIMIT, 'websocket_read_limit': self.config.WEBSOCKET_READ_LIMIT,

View File

@ -39,6 +39,7 @@ class Config(dict):
self.WEBSOCKET_READ_LIMIT = 2 ** 16 self.WEBSOCKET_READ_LIMIT = 2 ** 16
self.WEBSOCKET_WRITE_LIMIT = 2 ** 16 self.WEBSOCKET_WRITE_LIMIT = 2 ** 16
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec
self.ACCESS_LOG = True
if load_env: if load_env:
prefix = SANIC_PREFIX if load_env is True else load_env prefix = SANIC_PREFIX if load_env is True else load_env

View File

@ -74,7 +74,14 @@ def kill_process_children_unix(pid):
with open(children_proc_path) as children_list_file_2: with open(children_proc_path) as children_list_file_2:
children_list_pid_2 = children_list_file_2.read().split() children_list_pid_2 = children_list_file_2.read().split()
for _pid in children_list_pid_2: 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): def kill_process_children_osx(pid):
@ -94,7 +101,7 @@ def kill_process_children(pid):
""" """
if sys.platform == 'darwin': if sys.platform == 'darwin':
kill_process_children_osx(pid) kill_process_children_osx(pid)
elif sys.platform == 'posix': elif sys.platform == 'linux':
kill_process_children_unix(pid) kill_process_children_unix(pid)
else: else:
pass # should signal error here pass # should signal error here
@ -136,8 +143,8 @@ def watchdog(sleep_interval):
continue continue
elif mtime > old_time: elif mtime > old_time:
kill_process_children(worker_process.pid) kill_process_children(worker_process.pid)
worker_process.terminate()
worker_process = restart_with_reloader() worker_process = restart_with_reloader()
mtimes[filename] = mtime mtimes[filename] = mtime
break break

View File

@ -46,7 +46,7 @@ class BaseHTTPResponse:
class StreamingHTTPResponse(BaseHTTPResponse): class StreamingHTTPResponse(BaseHTTPResponse):
__slots__ = ( __slots__ = (
'transport', 'streaming_fn', 'status', 'protocol', 'streaming_fn', 'status',
'content_type', 'headers', '_cookies' 'content_type', 'headers', '_cookies'
) )
@ -58,7 +58,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
self.headers = CIMultiDict(headers or {}) self.headers = CIMultiDict(headers or {})
self._cookies = None self._cookies = None
def write(self, data): async def write(self, data):
"""Writes a chunk of data to the streaming response. """Writes a chunk of data to the streaming response.
:param data: bytes-ish data to be written. :param data: bytes-ish data to be written.
@ -66,8 +66,9 @@ class StreamingHTTPResponse(BaseHTTPResponse):
if type(data) != bytes: if type(data) != bytes:
data = self._encode_body(data) data = self._encode_body(data)
self.transport.write( self.protocol.push_data(
b"%x\r\n%b\r\n" % (len(data), data)) b"%x\r\n%b\r\n" % (len(data), data))
await self.protocol.drain()
async def stream( async def stream(
self, version="1.1", keep_alive=False, keep_alive_timeout=None): self, version="1.1", keep_alive=False, keep_alive_timeout=None):
@ -77,10 +78,12 @@ class StreamingHTTPResponse(BaseHTTPResponse):
headers = self.get_headers( headers = self.get_headers(
version, keep_alive=keep_alive, version, keep_alive=keep_alive,
keep_alive_timeout=keep_alive_timeout) keep_alive_timeout=keep_alive_timeout)
self.transport.write(headers) self.protocol.push_data(headers)
await self.protocol.drain()
await self.streaming_fn(self) 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( def get_headers(
self, version="1.1", keep_alive=False, keep_alive_timeout=None): 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") content_type="text/html; charset=utf-8")
async def file( async def file(location, status=200, mime_type=None, headers=None,
location, mime_type=None, headers=None, filename=None, _range=None): filename=None, _range=None):
"""Return a response object with file data. """Return a response object with file data.
:param location: Location of file on system. :param location: Location of file on system.
@ -260,15 +263,14 @@ async def file(
out_stream = await _file.read() out_stream = await _file.read()
mime_type = mime_type or guess_type(filename)[0] or 'text/plain' mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
return HTTPResponse(status=200, return HTTPResponse(status=status,
headers=headers, headers=headers,
content_type=mime_type, content_type=mime_type,
body_bytes=out_stream) body_bytes=out_stream)
async def file_stream( async def file_stream(location, status=200, chunk_size=4096, mime_type=None,
location, chunk_size=4096, mime_type=None, headers=None, headers=None, filename=None, _range=None):
filename=None, _range=None):
"""Return a streaming response object with file data. """Return a streaming response object with file data.
:param location: Location of file on system. :param location: Location of file on system.
@ -299,13 +301,13 @@ async def file_stream(
if len(content) < 1: if len(content) < 1:
break break
to_send -= len(content) to_send -= len(content)
response.write(content) await response.write(content)
else: else:
while True: while True:
content = await _file.read(chunk_size) content = await _file.read(chunk_size)
if len(content) < 1: if len(content) < 1:
break break
response.write(content) await response.write(content)
finally: finally:
await _file.close() await _file.close()
return # Returning from this fn closes the stream return # Returning from this fn closes the stream
@ -315,7 +317,7 @@ async def file_stream(
headers['Content-Range'] = 'bytes %s-%s/%s' % ( headers['Content-Range'] = 'bytes %s-%s/%s' % (
_range.start, _range.end, _range.total) _range.start, _range.end, _range.total)
return StreamingHTTPResponse(streaming_fn=_streaming_fn, return StreamingHTTPResponse(streaming_fn=_streaming_fn,
status=200, status=status,
headers=headers, headers=headers,
content_type=mime_type) content_type=mime_type)

View File

@ -55,7 +55,8 @@ class HttpProtocol(asyncio.Protocol):
# connection management # connection management
'_total_request_size', '_request_timeout_handler', '_total_request_size', '_request_timeout_handler',
'_response_timeout_handler', '_keep_alive_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, def __init__(self, *, loop, request_handler, error_handler,
signal=Signal(), connections=set(), request_timeout=60, signal=Signal(), connections=set(), request_timeout=60,
@ -82,6 +83,7 @@ class HttpProtocol(asyncio.Protocol):
self.request_class = request_class or Request self.request_class = request_class or Request
self.is_request_stream = is_request_stream self.is_request_stream = is_request_stream
self._is_stream_handler = False self._is_stream_handler = False
self._not_paused = asyncio.Event(loop=loop)
self._total_request_size = 0 self._total_request_size = 0
self._request_timeout_handler = None self._request_timeout_handler = None
self._response_timeout_handler = None self._response_timeout_handler = None
@ -96,6 +98,7 @@ class HttpProtocol(asyncio.Protocol):
if 'requests_count' not in self.state: if 'requests_count' not in self.state:
self.state['requests_count'] = 0 self.state['requests_count'] = 0
self._debug = debug self._debug = debug
self._not_paused.set()
@property @property
def keep_alive(self): def keep_alive(self):
@ -124,6 +127,12 @@ class HttpProtocol(asyncio.Protocol):
if self._keep_alive_timeout_handler: if self._keep_alive_timeout_handler:
self._keep_alive_timeout_handler.cancel() 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): def request_timeout_callback(self):
# See the docstring in the RequestTimeout exception, to see # See the docstring in the RequestTimeout exception, to see
# exactly what this timeout is checking for. # exactly what this timeout is checking for.
@ -351,6 +360,12 @@ class HttpProtocol(asyncio.Protocol):
self._last_response_time = current_time self._last_response_time = current_time
self.cleanup() 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): async def stream_response(self, response):
""" """
Streams a response to the client asynchronously. Attaches Streams a response to the client asynchronously. Attaches
@ -360,9 +375,10 @@ class HttpProtocol(asyncio.Protocol):
if self._response_timeout_handler: if self._response_timeout_handler:
self._response_timeout_handler.cancel() self._response_timeout_handler.cancel()
self._response_timeout_handler = None self._response_timeout_handler = None
try: try:
keep_alive = self.keep_alive keep_alive = self.keep_alive
response.transport = self.transport response.protocol = self
await response.stream( await response.stream(
self.request.version, keep_alive, self.keep_alive_timeout) self.request.version, keep_alive, self.keep_alive_timeout)
self.log_response(response) self.log_response(response)

View File

@ -19,7 +19,7 @@ from sanic.response import file, file_stream, HTTPResponse
def register(app, uri, file_or_directory, pattern, def register(app, uri, file_or_directory, pattern,
use_modified_since, use_content_range, use_modified_since, use_content_range,
stream_large_files, name='static', host=None, 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 # 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 # make a good effort here. Modified-since is nice, but we could
# also look into etags, expires, and caching # 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 If this is an integer, this represents the
threshold size to switch to file_stream() threshold size to switch to file_stream()
:param name: user defined name used for url_for :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, # If we're not trying to match a file directly,
# serve from the folder # serve from the folder
@ -95,10 +96,10 @@ def register(app, uri, file_or_directory, pattern,
del headers['Content-Length'] del headers['Content-Length']
for key, value in _range.headers.items(): for key, value in _range.headers.items():
headers[key] = value headers[key] = value
headers['Content-Type'] = content_type \
or guess_type(file_path)[0] or 'text/plain'
if request.method == 'HEAD': if request.method == 'HEAD':
return HTTPResponse( return HTTPResponse(headers=headers)
headers=headers,
content_type=guess_type(file_path)[0] or 'text/plain')
else: else:
if stream_large_files: if stream_large_files:
if isinstance(stream_large_files, int): if isinstance(stream_large_files, int):

26
tests/static/test.html Normal file
View File

@ -0,0 +1,26 @@
<html>
<body>
<pre>
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
▌ ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
▀▀█████████▀
▄▄██▀██████▀█
▄██▀ ▀▀▀ █
▄█ ▐▌
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
▌ ▐ ▀▀▄▄▄▀
▀▀▄▄▀
</pre>
</body>
</html>

View File

@ -1,5 +1,6 @@
import asyncio import asyncio
import inspect import inspect
import os
import pytest import pytest
from sanic import Sanic from sanic import Sanic
@ -13,6 +14,14 @@ from sanic.constants import HTTP_METHODS
# GET # 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) @pytest.mark.parametrize('method', HTTP_METHODS)
def test_versioned_routes_get(method): def test_versioned_routes_get(method):
app = Sanic('test_shorhand_routes_get') app = Sanic('test_shorhand_routes_get')
@ -348,6 +357,28 @@ def test_bp_static():
assert response.status == 200 assert response.status == 200
assert response.body == current_file_contents 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(): def test_bp_shorthand():
app = Sanic('test_shorhand_routes') app = Sanic('test_shorhand_routes')
blueprint = Blueprint('test_shorhand_routes') blueprint = Blueprint('test_shorhand_routes')
@ -449,41 +480,41 @@ def test_bp_shorthand():
def test_bp_group(): def test_bp_group():
app = Sanic('test_nested_bp_groups') app = Sanic('test_nested_bp_groups')
deep_0 = Blueprint('deep_0', url_prefix='/deep') deep_0 = Blueprint('deep_0', url_prefix='/deep')
deep_1 = Blueprint('deep_1', url_prefix = '/deep1') deep_1 = Blueprint('deep_1', url_prefix = '/deep1')
@deep_0.route('/') @deep_0.route('/')
def handler(request): def handler(request):
return text('D0_OK') return text('D0_OK')
@deep_1.route('/bottom') @deep_1.route('/bottom')
def handler(request): def handler(request):
return text('D1B_OK') return text('D1B_OK')
mid_0 = Blueprint.group(deep_0, deep_1, url_prefix='/mid') mid_0 = Blueprint.group(deep_0, deep_1, url_prefix='/mid')
mid_1 = Blueprint('mid_tier', url_prefix='/mid1') mid_1 = Blueprint('mid_tier', url_prefix='/mid1')
@mid_1.route('/') @mid_1.route('/')
def handler(request): def handler(request):
return text('M1_OK') return text('M1_OK')
top = Blueprint.group(mid_0, mid_1) top = Blueprint.group(mid_0, mid_1)
app.blueprint(top) app.blueprint(top)
@app.route('/') @app.route('/')
def handler(request): def handler(request):
return text('TOP_OK') return text('TOP_OK')
request, response = app.test_client.get('/') request, response = app.test_client.get('/')
assert response.text == 'TOP_OK' assert response.text == 'TOP_OK'
request, response = app.test_client.get('/mid1') request, response = app.test_client.get('/mid1')
assert response.text == 'M1_OK' assert response.text == 'M1_OK'
request, response = app.test_client.get('/mid/deep') request, response = app.test_client.get('/mid/deep')
assert response.text == 'D0_OK' assert response.text == 'D0_OK'
request, response = app.test_client.get('/mid/deep1/bottom') request, response = app.test_client.get('/mid/deep1/bottom')
assert response.text == 'D1B_OK' assert response.text == 'D1B_OK'

View File

@ -9,14 +9,39 @@ import aiohttp
from aiohttp import TCPConnector from aiohttp import TCPConnector
from sanic.testing import SanicTestClient, HOST, PORT 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): class ReuseableTCPConnector(TCPConnector):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ReuseableTCPConnector, self).__init__(*args, **kwargs) super(ReuseableTCPConnector, self).__init__(*args, **kwargs)
self.old_proto = None 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): async def connect(self, req, traces=None):
new_conn = await super(ReuseableTCPConnector, self)\ new_conn = await super(ReuseableTCPConnector, self)\
.connect(req, traces=traces) .connect(req, traces=traces)
@ -28,7 +53,6 @@ class ReuseableTCPConnector(TCPConnector):
self.old_proto = new_conn._protocol self.old_proto = new_conn._protocol
return new_conn return new_conn
else: else:
async def connect(self, req): async def connect(self, req):
new_conn = await super(ReuseableTCPConnector, self)\ new_conn = await super(ReuseableTCPConnector, self)\
.connect(req) .connect(req)

View File

@ -83,7 +83,7 @@ def test_request_stream_app():
body = await request.stream.get() body = await request.stream.get()
if body is None: if body is None:
break break
response.write(body.decode('utf-8')) await response.write(body.decode('utf-8'))
return stream(streaming) return stream(streaming)
@app.put('/_put') @app.put('/_put')
@ -100,7 +100,7 @@ def test_request_stream_app():
body = await request.stream.get() body = await request.stream.get()
if body is None: if body is None:
break break
response.write(body.decode('utf-8')) await response.write(body.decode('utf-8'))
return stream(streaming) return stream(streaming)
@app.patch('/_patch') @app.patch('/_patch')
@ -117,7 +117,7 @@ def test_request_stream_app():
body = await request.stream.get() body = await request.stream.get()
if body is None: if body is None:
break break
response.write(body.decode('utf-8')) await response.write(body.decode('utf-8'))
return stream(streaming) return stream(streaming)
assert app.is_request_stream is True assert app.is_request_stream is True
@ -177,7 +177,7 @@ def test_request_stream_handle_exception():
body = await request.stream.get() body = await request.stream.get()
if body is None: if body is None:
break break
response.write(body.decode('utf-8')) await response.write(body.decode('utf-8'))
return stream(streaming) return stream(streaming)
# 404 # 404
@ -231,7 +231,7 @@ def test_request_stream_blueprint():
body = await request.stream.get() body = await request.stream.get()
if body is None: if body is None:
break break
response.write(body.decode('utf-8')) await response.write(body.decode('utf-8'))
return stream(streaming) return stream(streaming)
@bp.put('/_put') @bp.put('/_put')
@ -248,7 +248,7 @@ def test_request_stream_blueprint():
body = await request.stream.get() body = await request.stream.get()
if body is None: if body is None:
break break
response.write(body.decode('utf-8')) await response.write(body.decode('utf-8'))
return stream(streaming) return stream(streaming)
@bp.patch('/_patch') @bp.patch('/_patch')
@ -265,7 +265,7 @@ def test_request_stream_blueprint():
body = await request.stream.get() body = await request.stream.get()
if body is None: if body is None:
break break
response.write(body.decode('utf-8')) await response.write(body.decode('utf-8'))
return stream(streaming) return stream(streaming)
app.blueprint(bp) app.blueprint(bp)
@ -380,7 +380,7 @@ def test_request_stream():
body = await request.stream.get() body = await request.stream.get()
if body is None: if body is None:
break break
response.write(body.decode('utf-8')) await response.write(body.decode('utf-8'))
return stream(streaming) return stream(streaming)
@app.get('/get') @app.get('/get')

View File

@ -5,9 +5,24 @@ import asyncio
from sanic.response import text from sanic.response import text
from sanic.config import Config from sanic.config import Config
import aiohttp import aiohttp
from aiohttp import TCPConnector from aiohttp import TCPConnector, ClientResponse
from sanic.testing import SanicTestClient, HOST, PORT 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): class DelayableTCPConnector(TCPConnector):
@ -38,8 +53,11 @@ class DelayableTCPConnector(TCPConnector):
self.orig_start = getattr(resp, 'start') self.orig_start = getattr(resp, 'start')
try: try:
ret = await self.orig_start(connection, if aiohttp_version >= version.parse("3.3.0"):
read_until_eof) ret = await self.orig_start(connection)
else:
ret = await self.orig_start(connection,
read_until_eof)
except Exception as e: except Exception as e:
raise e raise e
return ret return ret
@ -57,15 +75,31 @@ class DelayableTCPConnector(TCPConnector):
await asyncio.sleep(self.delay) await asyncio.sleep(self.delay)
t = req.loop.time() t = req.loop.time()
print("sending at {}".format(t), flush=True) print("sending at {}".format(t), flush=True)
conn = next(iter(args)) # first arg is connection conn = next(iter(args)) # first arg is connection
if aiohttp.__version__ >= "3.1.0":
if aiohttp_version >= version.parse("3.1.0"):
try: try:
delayed_resp = await self.orig_send(*args, **kwargs) delayed_resp = await self.orig_send(*args, **kwargs)
except Exception as e: except Exception as e:
return aiohttp.ClientResponse(req.method, req.url, if aiohttp_version >= version.parse("3.3.0"):
writer=None, continue100=None, timer=None, return aiohttp.ClientResponse(req.method, req.url,
request_info=None, auto_decompress=None, traces=[], writer=None,
loop=req.loop, session=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: else:
try: try:
delayed_resp = self.orig_send(*args, **kwargs) delayed_resp = self.orig_send(*args, **kwargs)
@ -73,7 +107,7 @@ class DelayableTCPConnector(TCPConnector):
return aiohttp.ClientResponse(req.method, req.url) return aiohttp.ClientResponse(req.method, req.url)
return delayed_resp 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 # aiohttp changed the request.send method to async
async def send(self, *args, **kwargs): async def send(self, *args, **kwargs):
gen = self.delayed_send(*args, **kwargs) gen = self.delayed_send(*args, **kwargs)
@ -96,12 +130,25 @@ class DelayableTCPConnector(TCPConnector):
self._post_connect_delay = _post_connect_delay self._post_connect_delay = _post_connect_delay
self._pre_request_delay = _pre_request_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): async def connect(self, req, traces=None):
d_req = DelayableTCPConnector.\ d_req = DelayableTCPConnector.\
RequestContextManager(req, self._pre_request_delay) 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: if self._post_connect_delay and self._post_connect_delay > 0:
await asyncio.sleep(self._post_connect_delay, await asyncio.sleep(self._post_connect_delay,
loop=self._loop) loop=self._loop)

View File

@ -10,6 +10,7 @@ from random import choice
from sanic import Sanic from sanic import Sanic
from sanic.response import HTTPResponse, stream, StreamingHTTPResponse, file, file_stream, json from sanic.response import HTTPResponse, stream, StreamingHTTPResponse, file, file_stream, json
from sanic.server import HttpProtocol
from sanic.testing import HOST, PORT from sanic.testing import HOST, PORT
from unittest.mock import MagicMock from unittest.mock import MagicMock
@ -30,9 +31,10 @@ def test_response_body_not_a_string():
async def sample_streaming_fn(response): async def sample_streaming_fn(response):
response.write('foo,') await response.write('foo,')
await asyncio.sleep(.001) await asyncio.sleep(.001)
response.write('bar') await response.write('bar')
def test_method_not_allowed(): 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): def test_stream_response_writes_correct_content_to_transport(streaming_app):
response = StreamingHTTPResponse(sample_streaming_fn) 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') @streaming_app.listener('after_server_start')
async def run_stream(app, loop): async def run_stream(app, loop):
await response.stream() 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' 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' 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' 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']) @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 = Sanic('test_file_helper')
@app.route('/files/<filename>', methods=['GET']) @app.route('/files/<filename>', methods=['GET'])
def file_route(request, filename): def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename) file_path = os.path.join(static_file_directory, filename)
file_path = os.path.abspath(unquote(file_path)) 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)) 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 response.body == get_file_content(static_file_directory, file_name)
assert 'Content-Disposition' not in response.headers assert 'Content-Disposition' not in response.headers

View File

@ -7,6 +7,7 @@ from sanic.config import Config
Config.RESPONSE_TIMEOUT = 1 Config.RESPONSE_TIMEOUT = 1
response_timeout_app = Sanic('test_response_timeout') response_timeout_app = Sanic('test_response_timeout')
response_timeout_default_app = Sanic('test_response_timeout_default') response_timeout_default_app = Sanic('test_response_timeout_default')
response_handler_cancelled_app = Sanic('test_response_handler_cancelled')
@response_timeout_app.route('/1') @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') request, response = response_timeout_default_app.test_client.get('/1')
assert response.status == 503 assert response.status == 503
assert response.text == 'Error: Response Timeout' 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

View File

@ -36,6 +36,21 @@ def test_static_file(static_file_directory, file_name):
assert response.body == get_file_content(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('file_name', ['test.file', 'decode me.txt'])
@pytest.mark.parametrize('base_uri', ['/static', '', '/dir']) @pytest.mark.parametrize('base_uri', ['/static', '', '/dir'])
def test_static_directory(file_name, base_uri, static_file_directory): def test_static_directory(file_name, base_uri, static_file_directory):