From d348bb4ff441057c99c83cb6d2769795d10f1377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=2E=20K=C3=A4rkk=C3=A4inen?= Date: Sun, 8 Mar 2020 16:56:22 +0200 Subject: [PATCH] Refactoring and cleanup. --- sanic/asgi.py | 90 +++++++---------------------------------------- sanic/http.py | 23 +++++------- sanic/request.py | 16 +++++---- sanic/response.py | 64 +++++++++++++++++++++------------ 4 files changed, 72 insertions(+), 121 deletions(-) diff --git a/sanic/asgi.py b/sanic/asgi.py index 52449819..8f379358 100644 --- a/sanic/asgi.py +++ b/sanic/asgi.py @@ -272,84 +272,20 @@ class ASGIApp: yield data def respond(self, response): - headers: List[Tuple[bytes, bytes]] = [] - cookies: Dict[str, str] = {} - try: - cookies = { - v.key: v - for _, v in list( - filter( - lambda item: item[0].lower() == "set-cookie", - response.headers.items(), - ) - ) - } - headers += [ - (str(name).encode("latin-1"), str(value).encode("latin-1")) - for name, value in response.headers.items() - if name.lower() not in ["set-cookie"] - ] - except AttributeError: - logger.error( - "Invalid response object for url %s, " - "Expected Type: HTTPResponse, Actual Type: %s", - self.request.url, - type(response), - ) - exception = ServerError("Invalid response type") - response = self.sanic_app.error_handler.response( - self.request, exception - ) - headers = [ - (str(name).encode("latin-1"), str(value).encode("latin-1")) - for name, value in response.headers.items() - if name not in (b"Set-Cookie",) - ] + response.stream, self.response = self, response + return response - if "content-length" not in response.headers and not isinstance( - response, StreamingHTTPResponse - ): - headers += [ - (b"content-length", str(len(response.body)).encode("latin-1")) - ] - - if "content-type" not in response.headers: - headers += [ - (b"content-type", str(response.content_type).encode("latin-1")) - ] - - if response.cookies: - cookies.update( - { - v.key: v - for _, v in response.cookies.items() - if v.key not in cookies.keys() - } - ) - - headers += [ - (b"set-cookie", cookie.encode("utf-8")) - for k, cookie in cookies.items() - ] - self.response_start = { - "type": "http.response.start", - "status": response.status, - "headers": headers, - } - self.response_body = response.body - return self - - async def send(self, data=None, end_stream=None): - if data is None is end_stream: - end_stream = True - if self.response_start: - await self.transport.send(self.response_start) - self.response_start = None - if self.response_body: - data = ( - self.response_body + data if data else self.response_body - ) - self.response_body = None + async def send(self, data, end_stream): + if self.response: + response, self.response = self.response, None + await self.transport.send({ + "type": "http.response.start", + "status": response.status, + "headers": response.full_headers, + }) + response_body = getattr(response, "body", None) + if response_body: + data = response_body + data if data else response_body await self.transport.send( { "type": "http.response.body", diff --git a/sanic/http.py b/sanic/http.py index 7896086f..97cdb0a8 100644 --- a/sanic/http.py +++ b/sanic/http.py @@ -77,7 +77,7 @@ class Http: if self.stage is Stage.HANDLER: raise ServerError("Handler produced no response") if self.stage is Stage.RESPONSE: - await self.send(end_stream=True) + await self.response.send(end_stream=True) except CancelledError: # Write an appropriate response before exiting e = self.exception or ServiceUnavailable(f"Cancelled") @@ -178,13 +178,15 @@ class Http: def http1_response_header(self, data, end_stream) -> bytes: res = self.response # Compatibility with simple response body - if not data and res.body: + if not data and getattr(res, "body", None): data, end_stream = res.body, True size = len(data) - status = res.status headers = res.headers if res.content_type and "content-type" not in headers: headers["content-type"] = res.content_type + status = res.status + if not isinstance(status, int) or status < 200: + raise RuntimeError(f"Invalid response status {status!r}") # Not Modified, Precondition Failed if status in (304, 412): headers = remove_entity_headers(headers) @@ -374,19 +376,10 @@ class Http: if self.stage is not Stage.HANDLER: self.stage = Stage.FAILED raise RuntimeError("Response already started") - if not isinstance(response.status, int) or response.status < 200: - raise RuntimeError(f"Invalid response status {response.status!r}") - self.response = response - return self + self.response, response.stream = response, self + return response - async def send(self, data=None, end_stream=None): - """Send any pending response headers and the given data as body. - :param data: str or bytes to be written - :end_stream: whether to close the stream after this block - """ - if data is None and end_stream is None: - end_stream = True - data = data.encode() if hasattr(data, "encode") else data or b"" + async def send(self, data, end_stream): data = self.response_func(data, end_stream) if not data: return diff --git a/sanic/request.py b/sanic/request.py index 4bb53556..7572fa76 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -106,15 +106,17 @@ class Request: ) def respond( - self, status=200, headers=None, content_type=DEFAULT_HTTP_CONTENT_TYPE + self, response=None, *, status=200, headers=None, content_type=None ): - return self.stream.respond( - status - if isinstance(status, HTTPResponse) - else HTTPResponse( - status=status, headers=headers, content_type=content_type, + # This logic of determining which response to use is subject to change + if response is None: + response = self.stream.response or HTTPResponse( + status=status, + headers=headers, + content_type=content_type, ) - ) + # Connect the response and return it + return self.stream.respond(response) async def receive_body(self): self.body = b"".join([data async for data in self.stream]) diff --git a/sanic/response.py b/sanic/response.py index 355f885e..da6929e6 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -26,7 +26,7 @@ class BaseHTTPResponse: return data.encode() if hasattr(data, "encode") else data def _parse_headers(self): - return format_http1(self.headers.items()) + return format_http1(self.full_headers) @property def cookies(self): @@ -34,6 +34,43 @@ class BaseHTTPResponse: self._cookies = CookieJar(self.headers) return self._cookies + @property + def full_headers(self): + """Obtain an encoded tuple of headers for a response to be sent.""" + headers = [] + cookies = {} + if self.content_type and not "content-type" in self.headers: + headers += (b"content-type", self.content_type.encode()), + for name, value in self.headers.items(): + name = f"{name}" + if name.lower() == "set-cookie": + cookies[value.key] = value + else: + headers += (name.encode("ascii"), f"{value}".encode()), + + if self.cookies: + cookies.update( + (v.key, v) + for v in self.cookies.values() + if v.key not in cookies + ) + + headers += [ + (b"set-cookie", cookie.encode("utf-8")) + for k, cookie in cookies.items() + ] + return headers + + async def send(self, data=None, end_stream=None): + """Send any pending response headers and the given data as body. + :param data: str or bytes to be written + :end_stream: whether to close the stream after this block + """ + if data is None and end_stream is None: + end_stream = True + data = data.encode() if hasattr(data, "encode") else data or b"" + await self.stream.send(data, end_stream=end_stream) + class StreamingHTTPResponse(BaseHTTPResponse): __slots__ = ( @@ -42,9 +79,7 @@ class StreamingHTTPResponse(BaseHTTPResponse): "status", "content_type", "headers", - "chunked", "_cookies", - "send", ) def __init__( @@ -53,13 +88,12 @@ class StreamingHTTPResponse(BaseHTTPResponse): status=200, headers=None, content_type="text/plain; charset=utf-8", - chunked=True, + chunked="deprecated", ): self.content_type = content_type self.streaming_fn = streaming_fn self.status = status self.headers = Header(headers or {}) - self.chunked = chunked self._cookies = None async def write(self, data): @@ -70,9 +104,7 @@ class StreamingHTTPResponse(BaseHTTPResponse): await self.send(self._encode_body(data)) async def stream(self, request): - self.send = request.respond( - self.status, self.headers, self.content_type, - ).send + request.respond(self) await self.streaming_fn(self) await self.send(end_stream=True) @@ -94,16 +126,6 @@ class HTTPResponse(BaseHTTPResponse): self.headers = Header(headers or {}) self._cookies = None - def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): - body = b"" - if has_message_body(self.status): - body = self.body - self.headers["Content-Length"] = self.headers.get( - "Content-Length", len(self.body) - ) - - return self.get_headers(version, keep_alive, keep_alive_timeout, body) - @property def cookies(self): if self._cookies is None: @@ -265,7 +287,7 @@ async def file_stream( mime_type=None, headers=None, filename=None, - chunked=True, + chunked="deprecated", _range=None, ): """Return a streaming response object with file data. @@ -314,7 +336,6 @@ async def file_stream( status=status, headers=headers, content_type=mime_type, - chunked=chunked, ) @@ -323,7 +344,7 @@ def stream( status=200, headers=None, content_type="text/plain; charset=utf-8", - chunked=True, + chunked="deprecated", ): """Accepts an coroutine `streaming_fn` which can be used to write chunks to a streaming response. Returns a `StreamingHTTPResponse`. @@ -349,7 +370,6 @@ def stream( headers=headers, content_type=content_type, status=status, - chunked=chunked, )