Merge branch 'main' into zhiwei/bp-copy
This commit is contained in:
commit
d4cd897522
@ -10,3 +10,15 @@ exclude_patterns:
|
|||||||
- "examples/"
|
- "examples/"
|
||||||
- "hack/"
|
- "hack/"
|
||||||
- "scripts/"
|
- "scripts/"
|
||||||
|
- "tests/"
|
||||||
|
checks:
|
||||||
|
argument-count:
|
||||||
|
enabled: false
|
||||||
|
file-lines:
|
||||||
|
config:
|
||||||
|
threshold: 1000
|
||||||
|
method-count:
|
||||||
|
config:
|
||||||
|
threshold: 40
|
||||||
|
complex-logic:
|
||||||
|
enabled: false
|
||||||
|
12
README.rst
12
README.rst
@ -77,17 +77,7 @@ The goal of the project is to provide a simple way to get up and running a highl
|
|||||||
Sponsor
|
Sponsor
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|Try CodeStream|
|
Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic.
|
||||||
|
|
||||||
.. |Try CodeStream| image:: https://alt-images.codestream.com/codestream_logo_sanicorg.png
|
|
||||||
:target: https://codestream.com/?utm_source=github&utm_campaign=sanicorg&utm_medium=banner
|
|
||||||
:alt: Try CodeStream
|
|
||||||
|
|
||||||
Manage pull requests and conduct code reviews in your IDE with full source-tree context. Comment on any line, not just the diffs. Use jump-to-definition, your favorite keybindings, and code intelligence with more of your workflow.
|
|
||||||
|
|
||||||
`Learn More <https://codestream.com/?utm_source=github&utm_campaign=sanicorg&utm_medium=banner>`_
|
|
||||||
|
|
||||||
Thank you to our sponsor. Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic.
|
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
|
12
sanic/app.py
12
sanic/app.py
@ -21,6 +21,7 @@ from traceback import format_exc
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
|
AnyStr,
|
||||||
Awaitable,
|
Awaitable,
|
||||||
Callable,
|
Callable,
|
||||||
Coroutine,
|
Coroutine,
|
||||||
@ -138,7 +139,7 @@ class Sanic(BaseSanic):
|
|||||||
log_config: Optional[Dict[str, Any]] = None,
|
log_config: Optional[Dict[str, Any]] = None,
|
||||||
configure_logging: bool = True,
|
configure_logging: bool = True,
|
||||||
register: Optional[bool] = None,
|
register: Optional[bool] = None,
|
||||||
dumps: Optional[Callable[..., str]] = None,
|
dumps: Optional[Callable[..., AnyStr]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(name=name)
|
super().__init__(name=name)
|
||||||
|
|
||||||
@ -193,7 +194,7 @@ class Sanic(BaseSanic):
|
|||||||
self.router.ctx.app = self
|
self.router.ctx.app = self
|
||||||
|
|
||||||
if dumps:
|
if dumps:
|
||||||
BaseHTTPResponse._dumps = dumps
|
BaseHTTPResponse._dumps = dumps # type: ignore
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def loop(self):
|
def loop(self):
|
||||||
@ -737,15 +738,14 @@ class Sanic(BaseSanic):
|
|||||||
request.route = route
|
request.route = route
|
||||||
|
|
||||||
if (
|
if (
|
||||||
request.stream.request_body # type: ignore
|
request.stream
|
||||||
|
and request.stream.request_body
|
||||||
and not route.ctx.ignore_body
|
and not route.ctx.ignore_body
|
||||||
):
|
):
|
||||||
|
|
||||||
if hasattr(handler, "is_stream"):
|
if hasattr(handler, "is_stream"):
|
||||||
# Streaming handler: lift the size limit
|
# Streaming handler: lift the size limit
|
||||||
request.stream.request_max_size = float( # type: ignore
|
request.stream.request_max_size = float("inf")
|
||||||
"inf"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# Non-streaming handler: preload body
|
# Non-streaming handler: preload body
|
||||||
await request.receive_body()
|
await request.receive_body()
|
||||||
|
@ -781,6 +781,7 @@ class RouteMixin:
|
|||||||
path={file_or_directory}, "
|
path={file_or_directory}, "
|
||||||
f"relative_url={__file_uri__}"
|
f"relative_url={__file_uri__}"
|
||||||
)
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
def _register_static(
|
def _register_static(
|
||||||
self,
|
self,
|
||||||
|
137
tests/test_http.py
Normal file
137
tests/test_http.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import asyncio
|
||||||
|
import json as stdjson
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
from textwrap import dedent
|
||||||
|
from typing import AnyStr
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from sanic_testing.reusable import ReusableClient
|
||||||
|
|
||||||
|
from sanic import json, text
|
||||||
|
from sanic.app import Sanic
|
||||||
|
|
||||||
|
|
||||||
|
PORT = 1234
|
||||||
|
|
||||||
|
|
||||||
|
class RawClient:
|
||||||
|
CRLF = b"\r\n"
|
||||||
|
|
||||||
|
def __init__(self, host: str, port: int):
|
||||||
|
self.reader = None
|
||||||
|
self.writer = None
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
self.reader, self.writer = await asyncio.open_connection(
|
||||||
|
self.host, self.port
|
||||||
|
)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
self.writer.close()
|
||||||
|
await self.writer.wait_closed()
|
||||||
|
|
||||||
|
async def send(self, message: AnyStr):
|
||||||
|
if isinstance(message, str):
|
||||||
|
msg = self._clean(message).encode("utf-8")
|
||||||
|
else:
|
||||||
|
msg = message
|
||||||
|
await self._send(msg)
|
||||||
|
|
||||||
|
async def _send(self, message: bytes):
|
||||||
|
if not self.writer:
|
||||||
|
raise Exception("No open write stream")
|
||||||
|
self.writer.write(message)
|
||||||
|
|
||||||
|
async def recv(self, nbytes: int = -1) -> bytes:
|
||||||
|
if not self.reader:
|
||||||
|
raise Exception("No open read stream")
|
||||||
|
return await self.reader.read(nbytes)
|
||||||
|
|
||||||
|
def _clean(self, message: str) -> str:
|
||||||
|
return (
|
||||||
|
dedent(message)
|
||||||
|
.lstrip("\n")
|
||||||
|
.replace("\n", self.CRLF.decode("utf-8"))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_app(app: Sanic):
|
||||||
|
app.config.KEEP_ALIVE_TIMEOUT = 1
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def base_handler(request):
|
||||||
|
return text("111122223333444455556666777788889999")
|
||||||
|
|
||||||
|
@app.post("/upload", stream=True)
|
||||||
|
async def upload_handler(request):
|
||||||
|
data = [part.decode("utf-8") async for part in request.stream]
|
||||||
|
return json(data)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def runner(test_app):
|
||||||
|
client = ReusableClient(test_app, port=PORT)
|
||||||
|
client.run()
|
||||||
|
yield client
|
||||||
|
client.stop()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(runner):
|
||||||
|
client = namedtuple("Client", ("raw", "send", "recv"))
|
||||||
|
|
||||||
|
raw = RawClient(runner.host, runner.port)
|
||||||
|
runner._run(raw.connect())
|
||||||
|
|
||||||
|
def send(msg):
|
||||||
|
nonlocal runner
|
||||||
|
nonlocal raw
|
||||||
|
runner._run(raw.send(msg))
|
||||||
|
|
||||||
|
def recv(**kwargs):
|
||||||
|
nonlocal runner
|
||||||
|
nonlocal raw
|
||||||
|
method = raw.recv_until if "until" in kwargs else raw.recv
|
||||||
|
return runner._run(method(**kwargs))
|
||||||
|
|
||||||
|
yield client(raw, send, recv)
|
||||||
|
|
||||||
|
runner._run(raw.close())
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_message(client):
|
||||||
|
client.send(
|
||||||
|
"""
|
||||||
|
GET / HTTP/1.1
|
||||||
|
host: localhost:7777
|
||||||
|
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
response = client.recv()
|
||||||
|
assert len(response) == 140
|
||||||
|
assert b"200 OK" in response
|
||||||
|
|
||||||
|
|
||||||
|
def test_transfer_chunked(client):
|
||||||
|
client.send(
|
||||||
|
"""
|
||||||
|
POST /upload HTTP/1.1
|
||||||
|
transfer-encoding: chunked
|
||||||
|
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
client.send(b"3\r\nfoo\r\n")
|
||||||
|
client.send(b"3\r\nbar\r\n")
|
||||||
|
client.send(b"0\r\n\r\n")
|
||||||
|
response = client.recv()
|
||||||
|
_, body = response.rsplit(b"\r\n\r\n", 1)
|
||||||
|
data = stdjson.loads(body)
|
||||||
|
|
||||||
|
assert data == ["foo", "bar"]
|
@ -461,6 +461,22 @@ def test_nested_dir(app, static_file_directory):
|
|||||||
assert response.text == "foo\n"
|
assert response.text == "foo\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_is_a_directory_error(app, static_file_directory):
|
||||||
|
error_text = "Is a directory. Access denied"
|
||||||
|
app.static("/static", static_file_directory)
|
||||||
|
|
||||||
|
@app.exception(Exception)
|
||||||
|
async def handleStaticDirError(request, exception):
|
||||||
|
if isinstance(exception, IsADirectoryError):
|
||||||
|
return text(error_text, status=403)
|
||||||
|
raise exception
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/static/")
|
||||||
|
|
||||||
|
assert response.status == 403
|
||||||
|
assert response.text == error_text
|
||||||
|
|
||||||
|
|
||||||
def test_stack_trace_on_not_found(app, static_file_directory, caplog):
|
def test_stack_trace_on_not_found(app, static_file_directory, caplog):
|
||||||
app.static("/static", static_file_directory)
|
app.static("/static", static_file_directory)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user