Merge branch 'master' into strict_markers_for_pytest
This commit is contained in:
commit
de3b40c2e6
|
@ -58,6 +58,8 @@ Sanic | Build fast. Run fast.
|
|||
|
||||
Sanic is a **Python 3.6+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy.
|
||||
|
||||
Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanic.readthedocs.io/en/latest/sanic/deploying.html#running-via-asgi>`_.
|
||||
|
||||
`Source code on GitHub <https://github.com/huge-success/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_.
|
||||
|
||||
The project is maintained by the community, for the community. **Contributions are welcome!**
|
||||
|
@ -104,7 +106,7 @@ Hello World Example
|
|||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
|
||||
Sanic can now be easily run using ``python3 hello.py``.
|
||||
Sanic can now be easily run using ``sanic hello.app``.
|
||||
|
||||
.. code::
|
||||
|
||||
|
|
3
changelogs/1904.feature.rst
Normal file
3
changelogs/1904.feature.rst
Normal file
|
@ -0,0 +1,3 @@
|
|||
Adds WEBSOCKET_PING_TIMEOUT and WEBSOCKET_PING_INTERVAL configuration values
|
||||
|
||||
Allows setting the ping_interval and ping_timeout arguments when initializing `WebSocketCommonProtocol`.
|
|
@ -98,7 +98,7 @@ The config files are regular Python files which are executed in order to load th
|
|||
Builtin Configuration Values
|
||||
----------------------------
|
||||
|
||||
Out of the box there are just a few predefined values which can be overwritten when creating the application.
|
||||
Out of the box there are just a few predefined values which can be overwritten when creating the application. Note that websocket configuration values will have no impact if running in ASGI mode.
|
||||
|
||||
+---------------------------+-------------------+-----------------------------------------------------------------------------+
|
||||
| Variable | Default | Description |
|
||||
|
@ -123,6 +123,10 @@ Out of the box there are just a few predefined values which can be overwritten w
|
|||
+---------------------------+-------------------+-----------------------------------------------------------------------------+
|
||||
| WEBSOCKET_WRITE_LIMIT | 2^16 | High-water limit of the buffer for outgoing bytes |
|
||||
+---------------------------+-------------------+-----------------------------------------------------------------------------+
|
||||
| WEBSOCKET_PING_INTERVAL | 20 | A Ping frame is sent every ping_interval seconds. |
|
||||
+---------------------------+-------------------+-----------------------------------------------------------------------------+
|
||||
| WEBSOCKET_PING_TIMEOUT | 20 | Connection is closed when Pong is not received after ping_timeout seconds |
|
||||
+---------------------------+-------------------+-----------------------------------------------------------------------------+
|
||||
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long to wait to force close non-idle connection (sec) |
|
||||
+---------------------------+-------------------+-----------------------------------------------------------------------------+
|
||||
| ACCESS_LOG | True | Disable or enable access log |
|
||||
|
|
|
@ -51,5 +51,9 @@ You could setup your own WebSocket configuration through ``app.config``, like
|
|||
app.config.WEBSOCKET_MAX_QUEUE = 32
|
||||
app.config.WEBSOCKET_READ_LIMIT = 2 ** 16
|
||||
app.config.WEBSOCKET_WRITE_LIMIT = 2 ** 16
|
||||
app.config.WEBSOCKET_PING_INTERVAL = 20
|
||||
app.config.WEBSOCKET_PING_TIMEOUT = 20
|
||||
|
||||
These settings will have no impact if running in ASGI mode.
|
||||
|
||||
Find more in ``Configuration`` section.
|
||||
|
|
|
@ -24,6 +24,8 @@ DEFAULT_CONFIG = {
|
|||
"WEBSOCKET_MAX_QUEUE": 32,
|
||||
"WEBSOCKET_READ_LIMIT": 2 ** 16,
|
||||
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
|
||||
"WEBSOCKET_PING_TIMEOUT": 20,
|
||||
"WEBSOCKET_PING_INTERVAL": 20,
|
||||
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
||||
"ACCESS_LOG": True,
|
||||
"FORWARDED_SECRET": None,
|
||||
|
|
|
@ -42,7 +42,7 @@ class BaseHTTPResponse:
|
|||
body=b"",
|
||||
):
|
||||
""".. deprecated:: 20.3:
|
||||
This function is not public API and will be removed."""
|
||||
This function is not public API and will be removed."""
|
||||
|
||||
# self.headers get priority over content_type
|
||||
if self.content_type and "Content-Type" not in self.headers:
|
||||
|
@ -249,7 +249,10 @@ def raw(
|
|||
:param content_type: the content type (string) of the response.
|
||||
"""
|
||||
return HTTPResponse(
|
||||
body=body, status=status, headers=headers, content_type=content_type,
|
||||
body=body,
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -452,7 +452,7 @@ class Router:
|
|||
return route_handler, [], kwargs, route.uri, route.name
|
||||
|
||||
def is_stream_handler(self, request):
|
||||
""" Handler for request is stream or not.
|
||||
"""Handler for request is stream or not.
|
||||
:param request: Request object
|
||||
:return: bool
|
||||
"""
|
||||
|
|
|
@ -14,11 +14,13 @@ from ipaddress import ip_address
|
|||
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
|
||||
from signal import signal as signal_func
|
||||
from time import time
|
||||
from typing import Type
|
||||
|
||||
from httptools import HttpRequestParser # type: ignore
|
||||
from httptools.parser.errors import HttpParserError # type: ignore
|
||||
|
||||
from sanic.compat import Header, ctrlc_workaround_for_windows
|
||||
from sanic.config import Config
|
||||
from sanic.exceptions import (
|
||||
HeaderExpectationFailed,
|
||||
InvalidUsage,
|
||||
|
@ -416,12 +418,13 @@ class HttpProtocol(asyncio.Protocol):
|
|||
async def stream_append(self):
|
||||
while self._body_chunks:
|
||||
body = self._body_chunks.popleft()
|
||||
if self.request.stream.is_full():
|
||||
self.transport.pause_reading()
|
||||
await self.request.stream.put(body)
|
||||
self.transport.resume_reading()
|
||||
else:
|
||||
await self.request.stream.put(body)
|
||||
if self.request:
|
||||
if self.request.stream.is_full():
|
||||
self.transport.pause_reading()
|
||||
await self.request.stream.put(body)
|
||||
self.transport.resume_reading()
|
||||
else:
|
||||
await self.request.stream.put(body)
|
||||
|
||||
def on_message_complete(self):
|
||||
# Entire request (headers and whole body) is received.
|
||||
|
@ -844,6 +847,7 @@ def serve(
|
|||
app.asgi = False
|
||||
|
||||
connections = connections if connections is not None else set()
|
||||
protocol_kwargs = _build_protocol_kwargs(protocol, app.config)
|
||||
server = partial(
|
||||
protocol,
|
||||
loop=loop,
|
||||
|
@ -852,6 +856,7 @@ def serve(
|
|||
app=app,
|
||||
state=state,
|
||||
unix=unix,
|
||||
**protocol_kwargs,
|
||||
)
|
||||
asyncio_server_kwargs = (
|
||||
asyncio_server_kwargs if asyncio_server_kwargs else {}
|
||||
|
@ -948,6 +953,21 @@ def serve(
|
|||
remove_unix_socket(unix)
|
||||
|
||||
|
||||
def _build_protocol_kwargs(
|
||||
protocol: Type[HttpProtocol], config: Config
|
||||
) -> dict:
|
||||
if hasattr(protocol, "websocket_timeout"):
|
||||
return {
|
||||
"max_size": config.WEBSOCKET_MAX_SIZE,
|
||||
"max_queue": config.WEBSOCKET_MAX_QUEUE,
|
||||
"read_limit": config.WEBSOCKET_READ_LIMIT,
|
||||
"write_limit": config.WEBSOCKET_WRITE_LIMIT,
|
||||
"ping_timeout": config.WEBSOCKET_PING_TIMEOUT,
|
||||
"ping_interval": config.WEBSOCKET_PING_INTERVAL,
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket:
|
||||
"""Create TCP server socket.
|
||||
:param host: IPv4, IPv6 or hostname may be specified
|
||||
|
|
|
@ -103,7 +103,9 @@ class SanicTestClient:
|
|||
|
||||
if self.port:
|
||||
server_kwargs = dict(
|
||||
host=host or self.host, port=self.port, **server_kwargs,
|
||||
host=host or self.host,
|
||||
port=self.port,
|
||||
**server_kwargs,
|
||||
)
|
||||
host, port = host or self.host, self.port
|
||||
else:
|
||||
|
|
|
@ -35,6 +35,8 @@ class WebSocketProtocol(HttpProtocol):
|
|||
websocket_max_queue=None,
|
||||
websocket_read_limit=2 ** 16,
|
||||
websocket_write_limit=2 ** 16,
|
||||
websocket_ping_interval=20,
|
||||
websocket_ping_timeout=20,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
@ -45,6 +47,8 @@ class WebSocketProtocol(HttpProtocol):
|
|||
self.websocket_max_queue = websocket_max_queue
|
||||
self.websocket_read_limit = websocket_read_limit
|
||||
self.websocket_write_limit = websocket_write_limit
|
||||
self.websocket_ping_interval = websocket_ping_interval
|
||||
self.websocket_ping_timeout = websocket_ping_timeout
|
||||
|
||||
# timeouts make no sense for websocket routes
|
||||
def request_timeout_callback(self):
|
||||
|
@ -119,6 +123,8 @@ class WebSocketProtocol(HttpProtocol):
|
|||
max_queue=self.websocket_max_queue,
|
||||
read_limit=self.websocket_read_limit,
|
||||
write_limit=self.websocket_write_limit,
|
||||
ping_interval=self.websocket_ping_interval,
|
||||
ping_timeout=self.websocket_ping_timeout,
|
||||
)
|
||||
# Following two lines are required for websockets 8.x
|
||||
self.websocket.is_client = False
|
||||
|
|
|
@ -174,7 +174,7 @@ class GunicornWorker(base.Worker):
|
|||
|
||||
@staticmethod
|
||||
def _create_ssl_context(cfg):
|
||||
""" Creates SSLContext instance for usage in asyncio.create_server.
|
||||
"""Creates SSLContext instance for usage in asyncio.create_server.
|
||||
See ssl.SSLSocket.__init__ for more details.
|
||||
"""
|
||||
ctx = ssl.SSLContext(cfg.ssl_version)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
from inspect import isawaitable
|
||||
|
||||
|
@ -148,6 +149,35 @@ def test_app_enable_websocket(app, websocket_enabled, enable):
|
|||
assert app.websocket_enabled == True
|
||||
|
||||
|
||||
@patch("sanic.app.WebSocketProtocol")
|
||||
def test_app_websocket_parameters(websocket_protocol_mock, app):
|
||||
app.config.WEBSOCKET_MAX_SIZE = 44
|
||||
app.config.WEBSOCKET_MAX_QUEUE = 45
|
||||
app.config.WEBSOCKET_READ_LIMIT = 46
|
||||
app.config.WEBSOCKET_WRITE_LIMIT = 47
|
||||
app.config.WEBSOCKET_PING_TIMEOUT = 48
|
||||
app.config.WEBSOCKET_PING_INTERVAL = 50
|
||||
|
||||
@app.websocket("/ws")
|
||||
async def handler(request, ws):
|
||||
await ws.send("test")
|
||||
|
||||
try:
|
||||
# This will fail because WebSocketProtocol is mocked and only the call kwargs matter
|
||||
app.test_client.get("/ws")
|
||||
except:
|
||||
pass
|
||||
|
||||
websocket_protocol_call_args = websocket_protocol_mock.call_args
|
||||
ws_kwargs = websocket_protocol_call_args[1]
|
||||
assert ws_kwargs["max_size"] == app.config.WEBSOCKET_MAX_SIZE
|
||||
assert ws_kwargs["max_queue"] == app.config.WEBSOCKET_MAX_QUEUE
|
||||
assert ws_kwargs["read_limit"] == app.config.WEBSOCKET_READ_LIMIT
|
||||
assert ws_kwargs["write_limit"] == app.config.WEBSOCKET_WRITE_LIMIT
|
||||
assert ws_kwargs["ping_timeout"] == app.config.WEBSOCKET_PING_TIMEOUT
|
||||
assert ws_kwargs["ping_interval"] == app.config.WEBSOCKET_PING_INTERVAL
|
||||
|
||||
|
||||
def test_handle_request_with_nested_exception(app, monkeypatch):
|
||||
|
||||
err_msg = "Mock Exception"
|
||||
|
|
|
@ -244,8 +244,8 @@ async def handler3(request):
|
|||
|
||||
def test_keep_alive_timeout_reuse():
|
||||
"""If the server keep-alive timeout and client keep-alive timeout are
|
||||
both longer than the delay, the client _and_ server will successfully
|
||||
reuse the existing connection."""
|
||||
both longer than the delay, the client _and_ server will successfully
|
||||
reuse the existing connection."""
|
||||
try:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
|
|
@ -46,8 +46,8 @@ def test_custom_context(app):
|
|||
invalid = str(e)
|
||||
|
||||
j = loads(response.body)
|
||||
j['response_mw_valid'] = user
|
||||
j['response_mw_invalid'] = invalid
|
||||
j["response_mw_valid"] = user
|
||||
j["response_mw_invalid"] = invalid
|
||||
return json(j)
|
||||
|
||||
request, response = app.test_client.get("/")
|
||||
|
@ -59,8 +59,7 @@ def test_custom_context(app):
|
|||
"has_missing": False,
|
||||
"invalid": "'types.SimpleNamespace' object has no attribute 'missing'",
|
||||
"response_mw_valid": "sanic",
|
||||
"response_mw_invalid":
|
||||
"'types.SimpleNamespace' object has no attribute 'missing'"
|
||||
"response_mw_invalid": "'types.SimpleNamespace' object has no attribute 'missing'",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import pytest
|
||||
import asyncio
|
||||
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.exceptions import HeaderExpectationFailed
|
||||
|
@ -6,6 +7,7 @@ from sanic.request import StreamBuffer
|
|||
from sanic.response import json, stream, text
|
||||
from sanic.views import CompositionView, HTTPMethodView
|
||||
from sanic.views import stream as stream_decorator
|
||||
from sanic.server import HttpProtocol
|
||||
|
||||
|
||||
data = "abc" * 1_000_000
|
||||
|
@ -337,6 +339,22 @@ def test_request_stream_handle_exception(app):
|
|||
assert "Method GET not allowed for URL /post/random_id" in response.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_stream_unread(app):
|
||||
"""ensure no error is raised when leaving unread bytes in byte-buffer"""
|
||||
|
||||
err = None
|
||||
protocol = HttpProtocol(loop=asyncio.get_event_loop(), app=app)
|
||||
try:
|
||||
protocol.request = None
|
||||
protocol._body_chunks.append("this is a test")
|
||||
await protocol.stream_append()
|
||||
except AttributeError as e:
|
||||
err = e
|
||||
|
||||
assert err is None and not protocol._body_chunks
|
||||
|
||||
|
||||
def test_request_stream_blueprint(app):
|
||||
"""for self.is_request_stream = True"""
|
||||
bp = Blueprint("test_blueprint_request_stream_blueprint")
|
||||
|
|
Loading…
Reference in New Issue
Block a user