mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-21 15:00:57 +00:00
61f7f24256
Implements the remaining backlog features:
- Dependency injection: register providers with @api.dependency() and
declare them as view parameters by name. Supports sync/async functions
and generators (post-yield code runs as teardown after the response).
Providers taking a parameter receive the current Request. Resolved at
most once per request; path params take precedence.
- Per-route rate limiting via RateLimiter.limit decorator
- WebSocket before-request hooks can reject connections (closing the
socket skips the handler) and may now be sync functions
Also fixes bugs found along the way:
- float path convertor regex had an unescaped dot, matching garbage
like "1a5" and crashing with a 500
- literal route characters weren't regex-escaped (/file.json matched
/fileXjson)
- BackgroundQueue.results grew without bound; completed futures are
now pruned
- req.media("form") crashed when Content-Type was missing
- custom formats registered on api.formats were ignored; they now
thread through the router to request parsing and response negotiation
- Accept headers matching encode-incapable formats (e.g. form) returned
an empty body; negotiation now falls through to JSON
Performance: Request.url and Request.params are cached; format
registries are no longer rebuilt twice per request.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
212 lines
4.6 KiB
Python
212 lines
4.6 KiB
Python
"""Tests for dependency injection in route handlers."""
|
|
|
|
import pytest
|
|
|
|
|
|
def test_sync_function_dependency(api):
|
|
@api.dependency()
|
|
def greeting():
|
|
return "hello"
|
|
|
|
@api.route("/")
|
|
def view(req, resp, *, greeting):
|
|
resp.text = greeting
|
|
|
|
r = api.requests.get("/")
|
|
assert r.text == "hello"
|
|
|
|
|
|
def test_async_function_dependency(api):
|
|
@api.dependency()
|
|
async def number():
|
|
return 42
|
|
|
|
@api.route("/")
|
|
async def view(req, resp, *, number):
|
|
resp.media = {"number": number}
|
|
|
|
r = api.requests.get("/")
|
|
assert r.json() == {"number": 42}
|
|
|
|
|
|
def test_bare_decorator(api):
|
|
@api.dependency
|
|
def token():
|
|
return "abc123"
|
|
|
|
@api.route("/")
|
|
def view(req, resp, *, token):
|
|
resp.text = token
|
|
|
|
r = api.requests.get("/")
|
|
assert r.text == "abc123"
|
|
|
|
|
|
def test_explicit_name(api):
|
|
@api.dependency(name="db")
|
|
def make_database():
|
|
return {"users": ["kenneth"]}
|
|
|
|
@api.route("/")
|
|
def view(req, resp, *, db):
|
|
resp.media = db
|
|
|
|
r = api.requests.get("/")
|
|
assert r.json() == {"users": ["kenneth"]}
|
|
|
|
|
|
def test_add_dependency(api):
|
|
api.add_dependency("config", lambda: {"debug": True})
|
|
|
|
@api.route("/")
|
|
def view(req, resp, *, config):
|
|
resp.media = config
|
|
|
|
r = api.requests.get("/")
|
|
assert r.json() == {"debug": True}
|
|
|
|
|
|
def test_sync_generator_teardown(api):
|
|
events = []
|
|
|
|
@api.dependency()
|
|
def resource():
|
|
events.append("setup")
|
|
yield "the-resource"
|
|
events.append("teardown")
|
|
|
|
@api.route("/")
|
|
def view(req, resp, *, resource):
|
|
events.append(f"handler:{resource}")
|
|
resp.text = resource
|
|
|
|
r = api.requests.get("/")
|
|
assert r.text == "the-resource"
|
|
assert events == ["setup", "handler:the-resource", "teardown"]
|
|
|
|
|
|
def test_async_generator_teardown(api):
|
|
events = []
|
|
|
|
@api.dependency()
|
|
async def conn():
|
|
events.append("setup")
|
|
yield "connection"
|
|
events.append("teardown")
|
|
|
|
@api.route("/")
|
|
async def view(req, resp, *, conn):
|
|
events.append("handler")
|
|
resp.text = conn
|
|
|
|
r = api.requests.get("/")
|
|
assert r.text == "connection"
|
|
assert events == ["setup", "handler", "teardown"]
|
|
|
|
|
|
def test_teardown_runs_when_handler_raises(api):
|
|
events = []
|
|
|
|
@api.dependency()
|
|
def resource():
|
|
events.append("setup")
|
|
yield "r"
|
|
events.append("teardown")
|
|
|
|
@api.route("/")
|
|
def view(req, resp, *, resource):
|
|
raise ValueError("boom")
|
|
|
|
with pytest.raises(ValueError):
|
|
api.requests.get("/")
|
|
assert events == ["setup", "teardown"]
|
|
|
|
|
|
def test_provider_receives_request(api):
|
|
@api.dependency()
|
|
def user_agent(req):
|
|
return req.headers.get("User-Agent", "unknown")
|
|
|
|
@api.route("/")
|
|
def view(req, resp, *, user_agent):
|
|
resp.text = user_agent
|
|
|
|
r = api.requests.get("/", headers={"User-Agent": "test-agent"})
|
|
assert r.text == "test-agent"
|
|
|
|
|
|
def test_dependency_with_path_params(api):
|
|
@api.dependency()
|
|
def db():
|
|
return {1: "kenneth", 2: "guido"}
|
|
|
|
@api.route("/users/{id:int}")
|
|
def view(req, resp, *, id, db):
|
|
resp.text = db[id]
|
|
|
|
r = api.requests.get("/users/2")
|
|
assert r.text == "guido"
|
|
|
|
|
|
def test_path_param_shadows_dependency(api):
|
|
@api.dependency(name="name")
|
|
def name_dep():
|
|
return "from-dependency"
|
|
|
|
@api.route("/{name}")
|
|
def view(req, resp, *, name):
|
|
resp.text = name
|
|
|
|
r = api.requests.get("/from-url")
|
|
assert r.text == "from-url"
|
|
|
|
|
|
def test_resolved_once_per_request(api):
|
|
instances = []
|
|
|
|
@api.dependency()
|
|
def tracker():
|
|
obj = object()
|
|
instances.append(obj)
|
|
return obj
|
|
|
|
@api.route("/")
|
|
class Resource:
|
|
def on_request(self, req, resp, *, tracker):
|
|
resp.headers["X-Seen"] = "request"
|
|
|
|
def on_get(self, req, resp, *, tracker):
|
|
resp.text = "ok"
|
|
|
|
r = api.requests.get("/")
|
|
assert r.text == "ok"
|
|
# Both views requested `tracker`, but the provider ran only once.
|
|
assert len(instances) == 1
|
|
|
|
|
|
def test_class_based_view_injection(api):
|
|
@api.dependency()
|
|
def store():
|
|
return ["a", "b"]
|
|
|
|
@api.route("/items")
|
|
class Items:
|
|
def on_get(self, req, resp, *, store):
|
|
resp.media = store
|
|
|
|
r = api.requests.get("/items")
|
|
assert r.json() == ["a", "b"]
|
|
|
|
|
|
def test_handler_without_dependencies_unaffected(api):
|
|
@api.dependency()
|
|
def unused():
|
|
raise AssertionError("should not be resolved")
|
|
|
|
@api.route("/")
|
|
def view(req, resp):
|
|
resp.text = "plain"
|
|
|
|
r = api.requests.get("/")
|
|
assert r.text == "plain"
|