Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5aed4c067 | ||
|
|
9e048bc0c3 | ||
|
|
5d7b0735ce | ||
|
|
7dbd3eb5e8 | ||
|
|
96364aacc0 | ||
|
|
fc18f86964 | ||
|
|
fb3d368a78 |
@@ -7,8 +7,8 @@
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from sanic import Sanic, response
|
||||
|
||||
from sanic import Sanic, response
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
@@ -42,9 +42,7 @@ async def handler_file(request):
|
||||
|
||||
@app.route("/file_stream")
|
||||
async def handler_file_stream(request):
|
||||
return await response.file_stream(
|
||||
Path("../") / "setup.py", chunk_size=1024
|
||||
)
|
||||
return await response.file_stream(Path("../") / "setup.py", chunk_size=1024)
|
||||
|
||||
|
||||
@app.route("/stream", stream=True)
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "20.9.0"
|
||||
__version__ = "20.9.1"
|
||||
|
||||
@@ -676,9 +676,10 @@ class Sanic:
|
||||
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||
URLs need to terminate with a */*
|
||||
:param content_type: user defined content type for header
|
||||
:return: None
|
||||
:return: routes registered on the router
|
||||
:rtype: List[sanic.router.Route]
|
||||
"""
|
||||
static_register(
|
||||
return static_register(
|
||||
self,
|
||||
uri,
|
||||
file_or_directory,
|
||||
|
||||
@@ -350,6 +350,8 @@ class ASGIApp:
|
||||
if name not in (b"Set-Cookie",)
|
||||
]
|
||||
|
||||
response.asgi = True
|
||||
|
||||
if "content-length" not in response.headers and not isinstance(
|
||||
response, StreamingHTTPResponse
|
||||
):
|
||||
|
||||
@@ -143,7 +143,18 @@ class Blueprint:
|
||||
if _routes:
|
||||
routes += _routes
|
||||
|
||||
# Static Files
|
||||
for future in self.statics:
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
_routes = app.static(
|
||||
uri, future.file_or_directory, *future.args, **future.kwargs
|
||||
)
|
||||
if _routes:
|
||||
routes += _routes
|
||||
|
||||
route_names = [route.name for route in routes if route]
|
||||
|
||||
# Middleware
|
||||
for future in self.middlewares:
|
||||
if future.args or future.kwargs:
|
||||
@@ -160,14 +171,6 @@ class Blueprint:
|
||||
for future in self.exceptions:
|
||||
app.exception(*future.args, **future.kwargs)(future.handler)
|
||||
|
||||
# Static Files
|
||||
for future in self.statics:
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
app.static(
|
||||
uri, future.file_or_directory, *future.args, **future.kwargs
|
||||
)
|
||||
|
||||
# Event listeners
|
||||
for event, listeners in self.listeners.items():
|
||||
for listener in listeners:
|
||||
|
||||
@@ -22,6 +22,9 @@ except ImportError:
|
||||
|
||||
|
||||
class BaseHTTPResponse:
|
||||
def __init__(self):
|
||||
self.asgi = False
|
||||
|
||||
def _encode_body(self, data):
|
||||
return data.encode() if hasattr(data, "encode") else data
|
||||
|
||||
@@ -80,6 +83,8 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
content_type="text/plain; charset=utf-8",
|
||||
chunked=True,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.content_type = content_type
|
||||
self.streaming_fn = streaming_fn
|
||||
self.status = status
|
||||
@@ -109,13 +114,14 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
"""
|
||||
if version != "1.1":
|
||||
self.chunked = False
|
||||
headers = self.get_headers(
|
||||
version,
|
||||
keep_alive=keep_alive,
|
||||
keep_alive_timeout=keep_alive_timeout,
|
||||
)
|
||||
await self.protocol.push_data(headers)
|
||||
await self.protocol.drain()
|
||||
if not getattr(self, "asgi", False):
|
||||
headers = self.get_headers(
|
||||
version,
|
||||
keep_alive=keep_alive,
|
||||
keep_alive_timeout=keep_alive_timeout,
|
||||
)
|
||||
await self.protocol.push_data(headers)
|
||||
await self.protocol.drain()
|
||||
await self.streaming_fn(self)
|
||||
if self.chunked:
|
||||
await self.protocol.push_data(b"0\r\n\r\n")
|
||||
@@ -143,6 +149,8 @@ class HTTPResponse(BaseHTTPResponse):
|
||||
content_type=None,
|
||||
body_bytes=b"",
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.content_type = content_type
|
||||
self.body = body_bytes if body is None else self._encode_body(body)
|
||||
self.status = status
|
||||
|
||||
@@ -134,6 +134,8 @@ def register(
|
||||
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
|
||||
:return: registered static routes
|
||||
:rtype: List[sanic.router.Route]
|
||||
"""
|
||||
# If we're not trying to match a file directly,
|
||||
# serve from the folder
|
||||
@@ -155,10 +157,11 @@ def register(
|
||||
)
|
||||
)
|
||||
|
||||
app.route(
|
||||
_routes, _ = app.route(
|
||||
uri,
|
||||
methods=["GET", "HEAD"],
|
||||
name=name,
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
)(_handler)
|
||||
return _routes
|
||||
|
||||
4
setup.py
4
setup.py
@@ -80,13 +80,13 @@ requirements = [
|
||||
ujson,
|
||||
"aiofiles>=0.3.0",
|
||||
"websockets>=8.1,<9.0",
|
||||
"multidict>=4.0,<5.0",
|
||||
"multidict==5.0.0",
|
||||
"httpx==0.15.4",
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
"pytest==5.2.1",
|
||||
"multidict>=4.0,<5.0",
|
||||
"multidict==5.0.0",
|
||||
"gunicorn",
|
||||
"pytest-cov",
|
||||
"httpcore==0.3.0",
|
||||
|
||||
@@ -735,6 +735,36 @@ def test_static_blueprint_name(app: Sanic, static_file_directory, file_name):
|
||||
_, response = app.test_client.get("/static/test.file/")
|
||||
assert response.status == 200
|
||||
|
||||
@pytest.mark.parametrize("file_name", ["test.file"])
|
||||
def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name):
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
with open(current_file, "rb") as file:
|
||||
file.read()
|
||||
|
||||
triggered = False
|
||||
|
||||
bp = Blueprint(name="test_mw", url_prefix="")
|
||||
|
||||
@bp.middleware('request')
|
||||
def bp_mw1(request):
|
||||
nonlocal triggered
|
||||
triggered = True
|
||||
|
||||
bp.static(
|
||||
"/test.file",
|
||||
get_file_path(static_file_directory, file_name),
|
||||
strict_slashes=True,
|
||||
name="static"
|
||||
)
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
uri = app.url_for("test_mw.static")
|
||||
assert uri == "/test.file"
|
||||
|
||||
_, response = app.test_client.get("/test.file")
|
||||
assert triggered is True
|
||||
|
||||
|
||||
def test_route_handler_add(app: Sanic):
|
||||
view = CompositionView()
|
||||
|
||||
@@ -10,7 +10,7 @@ from sanic.utils import load_module_from_file_location
|
||||
@pytest.fixture
|
||||
def loaded_module_from_file_location():
|
||||
return load_module_from_file_location(
|
||||
str(Path(__file__).parent / "static/app_test_config.py")
|
||||
str(Path(__file__).parent / "static" / "app_test_config.py")
|
||||
)
|
||||
|
||||
|
||||
@@ -20,10 +20,11 @@ def test_load_module_from_file_location(loaded_module_from_file_location):
|
||||
|
||||
|
||||
@pytest.mark.dependency(depends=["test_load_module_from_file_location"])
|
||||
def test_loaded_module_from_file_location_name(
|
||||
loaded_module_from_file_location,
|
||||
):
|
||||
assert loaded_module_from_file_location.__name__ == "app_test_config"
|
||||
def test_loaded_module_from_file_location_name(loaded_module_from_file_location,):
|
||||
name = loaded_module_from_file_location.__name__
|
||||
if "C:\\" in name:
|
||||
name = name.split("\\")[-1]
|
||||
assert name == "app_test_config"
|
||||
|
||||
|
||||
def test_load_module_from_file_location_with_non_existing_env_variable():
|
||||
|
||||
@@ -235,6 +235,12 @@ def test_chunked_streaming_returns_correct_content(streaming_app):
|
||||
assert response.text == "foo,bar"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chunked_streaming_returns_correct_content_asgi(streaming_app):
|
||||
request, response = await streaming_app.asgi_client.get("/")
|
||||
assert response.text == "4\r\nfoo,\r\n3\r\nbar\r\n0\r\n\r\n"
|
||||
|
||||
|
||||
def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
|
||||
request, response = non_chunked_streaming_app.test_client.get("/")
|
||||
assert "Transfer-Encoding" not in response.headers
|
||||
@@ -242,6 +248,16 @@ def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
|
||||
assert response.headers["Content-Length"] == "7"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_chunked_streaming_adds_correct_headers_asgi(
|
||||
non_chunked_streaming_app,
|
||||
):
|
||||
request, response = await non_chunked_streaming_app.asgi_client.get("/")
|
||||
assert "Transfer-Encoding" not in response.headers
|
||||
assert response.headers["Content-Type"] == "text/csv"
|
||||
assert response.headers["Content-Length"] == "7"
|
||||
|
||||
|
||||
def test_non_chunked_streaming_returns_correct_content(
|
||||
non_chunked_streaming_app,
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user