Merge branch 'main' into zhiwei/bp-copy
This commit is contained in:
commit
d4cd897522
@ -10,3 +10,15 @@ exclude_patterns:
|
||||
- "examples/"
|
||||
- "hack/"
|
||||
- "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
|
||||
-------
|
||||
|
||||
|Try CodeStream|
|
||||
|
||||
.. |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.
|
||||
Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
12
sanic/app.py
12
sanic/app.py
@ -21,6 +21,7 @@ from traceback import format_exc
|
||||
from types import SimpleNamespace
|
||||
from typing import (
|
||||
Any,
|
||||
AnyStr,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Coroutine,
|
||||
@ -138,7 +139,7 @@ class Sanic(BaseSanic):
|
||||
log_config: Optional[Dict[str, Any]] = None,
|
||||
configure_logging: bool = True,
|
||||
register: Optional[bool] = None,
|
||||
dumps: Optional[Callable[..., str]] = None,
|
||||
dumps: Optional[Callable[..., AnyStr]] = None,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
|
||||
@ -193,7 +194,7 @@ class Sanic(BaseSanic):
|
||||
self.router.ctx.app = self
|
||||
|
||||
if dumps:
|
||||
BaseHTTPResponse._dumps = dumps
|
||||
BaseHTTPResponse._dumps = dumps # type: ignore
|
||||
|
||||
@property
|
||||
def loop(self):
|
||||
@ -737,15 +738,14 @@ class Sanic(BaseSanic):
|
||||
request.route = route
|
||||
|
||||
if (
|
||||
request.stream.request_body # type: ignore
|
||||
request.stream
|
||||
and request.stream.request_body
|
||||
and not route.ctx.ignore_body
|
||||
):
|
||||
|
||||
if hasattr(handler, "is_stream"):
|
||||
# Streaming handler: lift the size limit
|
||||
request.stream.request_max_size = float( # type: ignore
|
||||
"inf"
|
||||
)
|
||||
request.stream.request_max_size = float("inf")
|
||||
else:
|
||||
# Non-streaming handler: preload body
|
||||
await request.receive_body()
|
||||
|
@ -781,6 +781,7 @@ class RouteMixin:
|
||||
path={file_or_directory}, "
|
||||
f"relative_url={__file_uri__}"
|
||||
)
|
||||
raise
|
||||
|
||||
def _register_static(
|
||||
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"
|
||||
|
||||
|
||||
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):
|
||||
app.static("/static", static_file_directory)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user