mirror of https://github.com/encode/starlette.git
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:
parent
4549d625f9
commit
0693a8f6c9
|
@ -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.
|
|
@ -18,6 +18,7 @@ nav:
|
|||
- Applications: 'applications.md'
|
||||
- Test Client: 'test_client.md'
|
||||
- Debugging: 'debugging.md'
|
||||
- Views: 'views.md'
|
||||
|
||||
markdown_extensions:
|
||||
- markdown.extensions.codehilite:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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"
|
Loading…
Reference in New Issue