Execution Context
Handlers need access to shared state — database connections, the current user, session data, application config. But they must not know which adapter (HTTP, WebSocket, bot, CLI) is calling them.
RoutingContext solves this: a simple container where the adapter stores
whatever the handlers need, and a ContextVar that makes it visible to
every RoutingClass in the current task.
RoutingContext in 30 seconds
from genro_routes import RoutingContext
# Create a context and attach attributes freely
ctx = RoutingContext()
ctx.db = db_connection
ctx.user = current_user
ctx.locale = "it"
No abstract methods, no required properties. Just set what you need.
Parent chain — layered contexts
Real applications have layers: a server starts, mounts an app, then handles requests. Each layer adds its own state without copying the parent’s:
# 1. Server boot — lives for the entire process
server_ctx = RoutingContext()
server_ctx.server = server
server_ctx.config = global_config
# 2. App mount — lives as long as the app is mounted
app_ctx = RoutingContext(parent=server_ctx)
app_ctx.app = app
# 3. Per-request — created and discarded for each request
request_ctx = RoutingContext(parent=app_ctx)
request_ctx.db = request.state.db
request_ctx.user = request.state.user
request_ctx.session = request.state.session
When a handler reads an attribute, lookup works bottom-up:
request_ctx.db → found locally → request.state.db
request_ctx.app → not local, check parent → app_ctx.app
request_ctx.config → not local, not in app_ctx, check grandparent → server_ctx.config
request_ctx.missing → not found anywhere → AttributeError
Setting an attribute locally shadows the parent — it does not modify it:
request_ctx.config = override # only this request sees the override
server_ctx.config # unchanged
Slot + parent chain — instance-scoped context
The context is stored in a _ctx slot on each RoutingClass instance.
Reading self.ctx walks up the _routing_parent chain until it finds a
non-None value — the same pattern used by _routing_parent itself.
Set it on the root — children inherit it automatically via the parent chain.
Override locally — a child can set its own
ctxto shadow the parent’s.Clear locally — setting
child.ctx = Nonemakes it fall through to the parent again.
How it works under the hood
# In routing.py (simplified)
class RoutingClass:
__slots__ = (..., "_ctx", ...)
@property
def ctx(self):
result = getattr(self, "_ctx", None)
if result is not None:
return result
parent = getattr(self, "_routing_parent", None)
if parent is not None:
return parent.ctx
return None
@ctx.setter
def ctx(self, value):
object.__setattr__(self, "_ctx", value)
Concurrency isolation (ContextVar for async, threading.local for threads) is the adapter’s responsibility, not genro-routes’.
Adapter usage pattern
An adapter (like genro-asgi) creates a per-request context and sets it on
any RoutingClass instance before dispatching:
# In your ASGI dispatcher
async def dispatch(request, service):
# Build a request-scoped context
ctx = RoutingContext(parent=app_ctx)
ctx.db = request.state.db
ctx.user = request.state.user
ctx.session = request.state.session
# Set it — now every RoutingClass in this task sees it
service.ctx = ctx
try:
result = await service.api.call("some_handler", ...)
finally:
service.ctx = None # cleanup for this task
After service.ctx = ctx, any handler can do:
@route("api")
def list_orders(self):
db = self.ctx.db # from request_ctx (local)
user = self.ctx.user # from request_ctx (local)
config = self.ctx.config # from server_ctx (walked up)
return db.query(...)
Database access
Before this design, a separate DbRoutingClass propagated db through the
routing hierarchy. Now db lives in the context like everything else:
# Old pattern (removed)
class MyServer(DbRoutingClass):
def __init__(self, db):
self.db = db # stored in __slots__, propagated via _routing_parent
# New pattern
server_ctx = RoutingContext()
server_ctx.db = db_connection
svc.ctx = server_ctx
# Handler access — same as before
@route("api")
def query(self):
return self.ctx.db.execute("SELECT 1")
If the adapter creates layered contexts, child contexts inherit db from
the parent automatically — no need to set it on every request if it’s the
same connection.
Subclassing RoutingContext
For adapters that prefer a more structured approach, subclassing works:
class ASGIContext(RoutingContext):
def __init__(self, request, app_ctx):
super().__init__(parent=app_ctx)
self._request = request
@property
def db(self):
return self._request.state.db
@property
def user(self):
return self._request.state.user
Properties defined on the subclass take precedence over __getattr__ parent
delegation. Both patterns (free attributes and subclass properties) can
coexist.
Summary
Concept |
How it works |
|---|---|
RoutingContext |
Simple object with free |
Parent chain |
|
Slot + parent chain |
|
Setting context |
|
Reading context |
|
Cleanup |
|
Database access |
|