Files
responder/tests/test_dependencies.py
T
kennethreitz 61f7f24256 Add dependency injection, per-route rate limiting, and WebSocket short-circuit
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>
2026-06-11 21:07:29 -04:00

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"