diff --git a/docs/views.md b/docs/views.md new file mode 100644 index 000000000..d276e0a56 --- /dev/null +++ b/docs/views.md @@ -0,0 +1,32 @@ + +Starlette includes a `View` class that provides a class-based view pattern which +handles HTTP method dispatching and provides additional structure for HTTP views. + +```python +from starlette import PlainTextResponse +from starlette.app import App +from starlette.views import View + + +app = App() + + +class HomepageView(View): + async def get(self, request, **kwargs): + response = PlainTextResponse(f"Hello, world!") + return response + + +class UserView(View): + async def get(self, request, **kwargs): + username = kwargs.get("username") + response = PlainTextResponse(f"Hello, {username}") + return response + + +app.add_route("/", HomepageView()) +app.add_route("/user/{username}", UserView()) +``` + +Class-based views will respond with "404 Not found" or "406 Method not allowed" +responses for requests which do not match. diff --git a/mkdocs.yml b/mkdocs.yml index f80ae1a4a..0b2c0dbd8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,6 +18,7 @@ nav: - Applications: 'applications.md' - Test Client: 'test_client.md' - Debugging: 'debugging.md' + - Views: 'views.md' markdown_extensions: - markdown.extensions.codehilite: diff --git a/starlette/views.py b/starlette/views.py new file mode 100644 index 000000000..ed20fbd5b --- /dev/null +++ b/starlette/views.py @@ -0,0 +1,21 @@ +from starlette.request import Request +from starlette.response import Response +from starlette.types import ASGIApp, ASGIInstance, Receive, Send, Scope + + +class View: + def __call__(self, scope: Scope, **kwargs) -> ASGIApp: + return self.dispatch(scope, **kwargs) + + def dispatch(self, scope: Scope, **kwargs) -> ASGIInstance: + request_method = scope["method"] if scope["method"] != "HEAD" else "GET" + func = getattr(self, request_method.lower(), None) + if func is None: + return Response("Not found", 404, media_type="text/plain") + + async def awaitable(receive: Receive, send: Send) -> None: + request = Request(scope, receive) + response = await func(request, **kwargs) + await response(receive, send) + + return awaitable diff --git a/tests/test_views.py b/tests/test_views.py new file mode 100644 index 000000000..36ff244b4 --- /dev/null +++ b/tests/test_views.py @@ -0,0 +1,50 @@ +import pytest +from starlette import App +from starlette.views import View +from starlette.response import PlainTextResponse +from starlette.testclient import TestClient + + +app = App() + + +class HomepageView(View): + async def get(self, request, **kwargs): + username = kwargs.get("username") + if username: + response = PlainTextResponse(f"Hello, {username}!") + else: + response = PlainTextResponse("Hello, world!") + return response + + +app.add_route("/", HomepageView()) +app.add_route("/user/{username}", HomepageView()) +app.add_route("/no-method", View()) + + +client = TestClient(app) + + +def test_route(): + response = client.get("/") + assert response.status_code == 200 + assert response.text == "Hello, world!" + + +def test_route_kwargs(): + response = client.get("/user/tomchristie") + assert response.status_code == 200 + assert response.text == "Hello, tomchristie!" + + +def test_route_method(): + response = client.post("/") + assert response.status_code == 406 + assert response.text == "Method not allowed" + + +def test_method_missing(): + response = client.get("/no-method") + assert response.status_code == 404 + assert response.text == "Not found"