Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Class based views #52

Merged
merged 8 commits into from
Aug 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"