Class based views (#52)

* 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
This commit is contained in:
Tom Christie 2018-08-30 13:53:37 +01:00 committed by GitHub
parent 4549d625f9
commit 0693a8f6c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 118 additions and 4 deletions

44
docs/views.md Normal file
View File

@ -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.

View File

@ -18,6 +18,7 @@ nav:
- Applications: 'applications.md'
- Test Client: 'test_client.md'
- Debugging: 'debugging.md'
- Views: 'views.md'
markdown_extensions:
- markdown.extensions.codehilite:

View File

@ -3,6 +3,7 @@ from starlette.routing import Path, PathPrefix, Router
from starlette.types import ASGIApp, ASGIInstance, Receive, Scope, Send
from starlette.websockets import WebSocketSession
import asyncio
import inspect
def request_response(func):
@ -52,24 +53,32 @@ class App:
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

22
starlette/views.py Normal file
View File

@ -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
tests/test_views.py Normal file
View File

@ -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"