diff --git a/sanic/request.py b/sanic/request.py index 9cab4017..dca15901 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -56,6 +56,14 @@ class StreamBuffer: self._queue.task_done() return payload + async def __aiter__(self): + """Support `async for data in request.stream`""" + while True: + data = await self.read() + if not data: + break + yield data + async def put(self, payload): await self._queue.put(payload) @@ -128,14 +136,33 @@ class Request: ) def body_init(self): + """.. deprecated:: 20.3""" self.body = [] def body_push(self, data): + """.. deprecated:: 20.3""" self.body.append(data) def body_finish(self): + """.. deprecated:: 20.3""" self.body = b"".join(self.body) + async def receive_body(self): + """Receive request.body, if not already received. + + Streaming handlers may call this to receive the full body. + + This is added as a compatibility shim in Sanic 20.3 because future + versions of Sanic will make all requests streaming and will use this + function instead of the non-async body_init/push/finish functions. + + Please make an issue if your code depends on the old functionality and + cannot be upgraded to the new API. + """ + if not self.stream: + return + self.body = b"".join([data async for data in self.stream]) + @property def json(self): if self.parsed_json is None: diff --git a/tests/test_request_stream.py b/tests/test_request_stream.py index b76e7248..17b0ac82 100644 --- a/tests/test_request_stream.py +++ b/tests/test_request_stream.py @@ -3,7 +3,7 @@ import pytest from sanic.blueprints import Blueprint from sanic.exceptions import HeaderExpectationFailed from sanic.request import StreamBuffer -from sanic.response import stream, text +from sanic.response import json, stream, text from sanic.views import CompositionView, HTTPMethodView from sanic.views import stream as stream_decorator @@ -613,3 +613,43 @@ def test_request_stream(app): request, response = app.test_client.post("/bp_stream", data=data) assert response.status == 200 assert response.text == data + +def test_streaming_new_api(app): + @app.post("/non-stream") + async def handler(request): + assert request.body == b"x" + await request.receive_body() # This should do nothing + assert request.body == b"x" + return text("OK") + + @app.post("/1", stream=True) + async def handler(request): + assert request.stream + assert not request.body + await request.receive_body() + return text(request.body.decode().upper()) + + @app.post("/2", stream=True) + async def handler(request): + ret = [] + async for data in request.stream: + # We should have no b"" or None, just proper chunks + assert data + assert isinstance(data, bytes) + ret.append(data.decode("ASCII")) + return json(ret) + + request, response = app.test_client.post("/non-stream", data="x") + assert response.status == 200 + + request, response = app.test_client.post("/1", data="TEST data") + assert request.body == b"TEST data" + assert response.status == 200 + assert response.text == "TEST DATA" + + request, response = app.test_client.post("/2", data=data) + assert response.status == 200 + res = response.json + assert isinstance(res, list) + assert len(res) > 1 + assert "".join(res) == data diff --git a/tests/test_response.py b/tests/test_response.py index aa72c3db..acb715db 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -15,6 +15,7 @@ from aiofiles import os as async_os from sanic.response import ( HTTPResponse, StreamingHTTPResponse, + empty, file, file_stream, json, @@ -22,7 +23,6 @@ from sanic.response import ( stream, text, ) -from sanic.response import empty from sanic.server import HttpProtocol from sanic.testing import HOST, PORT