diff --git a/README.md b/README.md index cb67f247..dcf5e1de 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,26 @@ class App: await response(receive, send) ``` +### RedirectResponse + +Returns an HTTP redirect. Uses a 302 status code by default. + +```python +from starlette import PlainTextResponse, RedirectResponse + + +class App: + def __init__(self, scope): + self.scope = scope + + async def __call__(self, receive, send): + if self.scope['path'] != '/': + response = RedirectResponse(url='/') + else: + response = PlainTextResponse('Hello, world!') + await response(receive, send) +``` + ### StreamingResponse Takes an async generator and streams the response body. diff --git a/starlette/__init__.py b/starlette/__init__.py index b8af8d3b..b6f4501f 100644 --- a/starlette/__init__.py +++ b/starlette/__init__.py @@ -3,6 +3,7 @@ from starlette.response import ( FileResponse, HTMLResponse, JSONResponse, + RedirectResponse, Response, PlainTextResponse, StreamingResponse, @@ -17,9 +18,10 @@ __all__ = ( "HTMLResponse", "JSONResponse", "PlainTextResponse", + "RedirectResponse", "Response", "StreamingResponse", "Request", "TestClient", ) -__version__ = "0.1.16" +__version__ = "0.1.17" diff --git a/starlette/datastructures.py b/starlette/datastructures.py index 0159d6e9..12ad3d28 100644 --- a/starlette/datastructures.py +++ b/starlette/datastructures.py @@ -49,6 +49,10 @@ class URL(str): def port(self): return self.components.port + def replace(self, **kwargs): + components = self.components._replace(**kwargs) + return URL(components.geturl()) + # Type annotations for valid `__init__` values to QueryParams and Headers. StrPairs = typing.Sequence[typing.Tuple[str, str]] diff --git a/starlette/response.py b/starlette/response.py index f189ef31..7e03c38f 100644 --- a/starlette/response.py +++ b/starlette/response.py @@ -3,6 +3,7 @@ from email.utils import formatdate from mimetypes import guess_type from starlette.datastructures import MutableHeaders from starlette.types import Receive, Send +from urllib.parse import quote_plus import aiofiles import json import hashlib @@ -98,6 +99,12 @@ class JSONResponse(Response): return json.dumps(content, **self.options).encode("utf-8") +class RedirectResponse(Response): + def __init__(self, url: str, status_code: int = 302, headers: dict = None) -> None: + super().__init__(content=b"", status_code=status_code, headers=headers) + self.headers["location"] = quote_plus(url, safe=":/#?&=@[]!$&'()*+,;") + + class StreamingResponse(Response): def __init__( self, diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 35daba2c..672feaa7 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -13,6 +13,9 @@ def test_url(): assert u.query == "abc=123" assert u.params == "" assert u.fragment == "anchor" + new = u.replace(scheme="http") + assert new == "http://example.org:123/path/to/somewhere?abc=123#anchor" + assert new.scheme == "http" def test_headers(): diff --git a/tests/test_response.py b/tests/test_response.py index bbfde256..0cf9f32a 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -1,4 +1,10 @@ -from starlette import FileResponse, Response, StreamingResponse, TestClient +from starlette.response import ( + FileResponse, + RedirectResponse, + Response, + StreamingResponse, +) +from starlette.testclient import TestClient import asyncio import os @@ -29,6 +35,23 @@ def test_bytes_response(): assert response.content == b"xxxxx" +def test_redirect_response(): + def app(scope): + async def asgi(receive, send): + if scope["path"] == "/": + response = Response("hello, world", media_type="text/plain") + else: + response = RedirectResponse("/") + await response(receive, send) + + return asgi + + client = TestClient(app) + response = client.get("/redirect") + assert response.text == "hello, world" + assert response.url == "http://testserver/" + + def test_streaming_response(): def app(scope): async def numbers(minimum, maximum):