Skip to content

Commit

Permalink
Class based views (#52)
Browse files Browse the repository at this point in the history
* Renaming asgi_application, implementing CBV pattern, add_route method on router

* Refactor view to allow both sync/async methods

* Type hints for CBV

* Implement asgi decorator method directly in view class, remove classmethod

* Refactor CBV, remove router add_route method in favor of App.add_route method, tests, documentation

* Include tests

* Add support for class-based views
  • Loading branch information
tomchristie authored Aug 30, 2018
1 parent 4549d62 commit 0693a8f
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 4 deletions.
44 changes: 44 additions & 0 deletions docs/views.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@

Starlette includes a `View` class that provides a class-based view pattern which
handles HTTP method dispatching.

The `View` class can be used as an other ASGI application:

```python
from starlette.response import PlainTextResponse
from starlette.views import View


class App(View):
async def get(self, request):
return PlainTextResponse(f"Hello, world!")
```

If you're using a Starlette application instance to handle routing, you can
dispatch to a View class by using the `@app.route()` decorator, or the
`app.add_route()` function. Make sure to dispatch to the class itself, rather
than to an instance of the class:

```python
from starlette.app import App
from starlette.response import PlainTextResponse
from starlette.views import View


app = App()


@app.route("/")
class Homepage(View):
async def get(self, request):
return PlainTextResponse(f"Hello, world!")


@app.route("/{username}")
class User(View):
async def get(self, request, username):
return PlainTextResponse(f"Hello, {username}")
```

Class-based views will respond with "406 Method not allowed" responses for any
request methods which do not map to a corresponding handler.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ nav:
- Applications: 'applications.md'
- Test Client: 'test_client.md'
- Debugging: 'debugging.md'
- Views: 'views.md'

markdown_extensions:
- markdown.extensions.codehilite:
Expand Down
17 changes: 13 additions & 4 deletions starlette/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from starlette.types import ASGIApp, ASGIInstance, Receive, Scope, Send
from starlette.websockets import WebSocketSession
import asyncio
import inspect


def request_response(func):
Expand Down Expand Up @@ -52,24 +53,32 @@ def mount(self, path: str, app: ASGIApp):
self.router.routes.append(prefix)

def add_route(self, path: str, route, methods=None) -> None:
if methods is None:
methods = ["GET"]
instance = Path(path, request_response(route), protocol="http", methods=methods)
if not inspect.isclass(route):
route = request_response(route)
if methods is None:
methods = ["GET"]

instance = Path(path, route, protocol="http", methods=methods)
self.router.routes.append(instance)

def add_websocket_route(self, path: str, route) -> None:
instance = Path(path, websocket_session(route), protocol="websocket")
if not inspect.isclass(route):
route = websocket_session(route)

instance = Path(path, route, protocol="websocket")
self.router.routes.append(instance)

def route(self, path: str):
def decorator(func):
self.add_route(path, func)
return func

return decorator

def websocket_route(self, path: str):
def decorator(func):
self.add_websocket_route(path, func)
return func

return decorator

Expand Down
22 changes: 22 additions & 0 deletions starlette/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from starlette.request import Request
from starlette.response import Response, PlainTextResponse
from starlette.types import Receive, Send, Scope


class View:
def __init__(self, scope: Scope):
self.scope = scope

async def __call__(self, receive: Receive, send: Send):
request = Request(self.scope, receive=receive)
kwargs = self.scope.get("kwargs", {})
response = await self.dispatch(request, **kwargs)
await response(receive, send)

async def dispatch(self, request: Request, **kwargs) -> Response:
handler_name = "get" if request.method == "HEAD" else request.method.lower()
handler = getattr(self, handler_name, self.method_not_allowed)
return await handler(request, **kwargs)

async def method_not_allowed(self, request: Request, **kwargs) -> Response:
return PlainTextResponse("Method not allowed", 406)
38 changes: 38 additions & 0 deletions tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import pytest
from starlette import App
from starlette.views import View
from starlette.response import PlainTextResponse
from starlette.testclient import TestClient


app = App()


@app.route("/")
@app.route("/{username}")
class Homepage(View):
async def get(self, request, username=None):
if username is None:
return PlainTextResponse("Hello, world!")
return PlainTextResponse(f"Hello, {username}!")


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("/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"

0 comments on commit 0693a8f

Please sign in to comment.