Implement dict-like API on request objects for custom data. (#1666)

* Implement dict-like API on request objects for custom data.
* Updated docs about custom_context.
This commit is contained in:
L. Kärkkäinen 2019-09-27 00:11:31 +03:00 committed by 7
parent 6fc3381229
commit c54a8b10bb
4 changed files with 106 additions and 21 deletions

View File

@ -39,8 +39,8 @@ app = Sanic(__name__)
@app.middleware('request') @app.middleware('request')
async def add_key(request): async def add_key(request):
# Add a key to request object like dict object # Arbitrary data may be stored in request context:
request['foo'] = 'bar' request.ctx.foo = 'bar'
@app.middleware('response') @app.middleware('response')
@ -53,16 +53,21 @@ async def prevent_xss(request, response):
response.headers["x-xss-protection"] = "1; mode=block" response.headers["x-xss-protection"] = "1; mode=block"
@app.get("/")
async def index(request):
return sanic.response.text(request.ctx.foo)
app.run(host="0.0.0.0", port=8000) app.run(host="0.0.0.0", port=8000)
``` ```
The above code will apply the three middleware in order. The first middleware The three middlewares are executed in order:
**add_key** will add a new key `foo` into `request` object. This worked because
`request` object can be manipulated like `dict` object. Then, the second middleware 1. The first request middleware **add_key** adds a new key `foo` into request context.
**custom_banner** will change the HTTP response header *Server* to 2. Request is routed to handler **index**, which gets the key from context and returns a text response.
*Fake-Server*, and the last middleware **prevent_xss** will add the HTTP 3. The first response middleware **custom_banner** changes the HTTP response header *Server* to
header for preventing Cross-Site-Scripting (XSS) attacks. These two functions say *Fake-Server*
are invoked *after* a user function returns a response. 4. The second response middleware **prevent_xss** adds the HTTP header for preventing Cross-Site-Scripting (XSS) attacks.
## Responding early ## Responding early
@ -81,6 +86,16 @@ async def halt_response(request, response):
return text('I halted the response') return text('I halted the response')
``` ```
## Custom context
Arbitrary data may be stored in `request.ctx`. A typical use case
would be to store the user object acquired from database in an authentication
middleware. Keys added are accessible to all later middleware as well as
the handler over the duration of the request.
Custom context is reserved for applications and extensions. Sanic itself makes
no use of it.
## Listeners ## Listeners
If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners: If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners:

View File

@ -4,6 +4,7 @@ import warnings
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
from types import SimpleNamespace
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
from httptools import parse_url # type: ignore from httptools import parse_url # type: ignore
@ -62,7 +63,7 @@ class StreamBuffer:
return self._queue.full() return self._queue.full()
class Request(dict): class Request:
"""Properties of an HTTP request such as URL, headers, etc.""" """Properties of an HTTP request such as URL, headers, etc."""
__slots__ = ( __slots__ = (
@ -75,6 +76,7 @@ class Request(dict):
"_socket", "_socket",
"app", "app",
"body", "body",
"ctx",
"endpoint", "endpoint",
"headers", "headers",
"method", "method",
@ -104,6 +106,7 @@ class Request(dict):
# Init but do not inhale # Init but do not inhale
self.body_init() self.body_init()
self.ctx = SimpleNamespace()
self.parsed_forwarded = None self.parsed_forwarded = None
self.parsed_json = None self.parsed_json = None
self.parsed_form = None self.parsed_form = None
@ -120,10 +123,30 @@ class Request(dict):
self.__class__.__name__, self.method, self.path self.__class__.__name__, self.method, self.path
) )
def __bool__(self): def get(self, key, default=None):
if self.transport: """.. deprecated:: 19.9
return True Custom context is now stored in `request.custom_context.yourkey`"""
return False return self.ctx.__dict__.get(key, default)
def __contains__(self, key):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
return key in self.ctx.__dict__
def __getitem__(self, key):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
return self.ctx.__dict__[key]
def __delitem__(self, key):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
del self.ctx.__dict__[key]
def __setitem__(self, key, value):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
setattr(self.ctx, key, value)
def body_init(self): def body_init(self):
self.body = [] self.body = []

View File

@ -8,22 +8,72 @@ try:
except ImportError: except ImportError:
from json import loads from json import loads
def test_custom_context(app):
def test_storage(app):
@app.middleware("request") @app.middleware("request")
def store(request): def store(request):
request.ctx.user = "sanic"
request.ctx.session = None
@app.route("/")
def handler(request):
# Accessing non-existant key should fail with AttributeError
try:
invalid = request.ctx.missing
except AttributeError as e:
invalid = str(e)
return json({
"user": request.ctx.user,
"session": request.ctx.session,
"has_user": hasattr(request.ctx, "user"),
"has_session": hasattr(request.ctx, "session"),
"has_missing": hasattr(request.ctx, "missing"),
"invalid": invalid
})
request, response = app.test_client.get("/")
assert response.json == {
"user": "sanic",
"session": None,
"has_user": True,
"has_session": True,
"has_missing": False,
"invalid": "'types.SimpleNamespace' object has no attribute 'missing'",
}
# Remove this once the deprecated API is abolished.
def test_custom_context_old(app):
@app.middleware("request")
def store(request):
try:
request["foo"]
except KeyError:
pass
request["user"] = "sanic" request["user"] = "sanic"
request["sidekick"] = "tails" sidekick = request.get("sidekick", "tails") # Item missing -> default
request["sidekick"] = sidekick
request["bar"] = request["sidekick"]
del request["sidekick"] del request["sidekick"]
@app.route("/") @app.route("/")
def handler(request): def handler(request):
return json( return json(
{"user": request.get("user"), "sidekick": request.get("sidekick")} {
"user": request.get("user"),
"sidekick": request.get("sidekick"),
"has_bar": "bar" in request,
"has_sidekick": "sidekick" in request,
}
) )
request, response = app.test_client.get("/") request, response = app.test_client.get("/")
assert response.json == {
"user": "sanic",
"sidekick": None,
"has_bar": True,
"has_sidekick": False,
}
response_json = loads(response.text) response_json = loads(response.text)
assert response_json["user"] == "sanic" assert response_json["user"] == "sanic"
assert response_json.get("sidekick") is None assert response_json.get("sidekick") is None

View File

@ -1499,9 +1499,6 @@ def test_request_bool(app):
request, response = app.test_client.get("/") request, response = app.test_client.get("/")
assert bool(request) assert bool(request)
request.transport = False
assert not bool(request)
def test_request_parsing_form_failed(app, caplog): def test_request_parsing_form_failed(app, caplog):
@app.route("/", methods=["POST"]) @app.route("/", methods=["POST"])