risa.view¶
Views, their handlers, and how they are registered.
HandlerFunction
¶
HandlerFunction = Callable[
Concatenate[V, ComponentContext, P], Awaitable[None]
]
A method @risa.handler can wrap: it takes the view, a ComponentContext and its own parameters.
AutoDefer
¶
Bases: StrEnum
How risa answers a click whose handler has not answered in time.
Discord fails an interaction that gets no answer within three seconds. With
auto-defer, risa acknowledges the click in the handler's place after the client's
auto_defer_delay, and the handler carries on. Set it on the client, on a view with
@risa.register(defer=...), or on a handler with @risa.handler(defer=...); the
most specific setting wins.
Attributes:
-
OFF–Never answer in the handler's place. The default.
-
UPDATE–Acknowledge silently, for a handler that answers with
ctx.rerender()orctx.edit(...). -
THINKING–Show a "thinking..." message, for a handler that answers with
ctx.respond(...), which then takes its place. -
THINKING_EPHEMERAL–The same, visible only to the user who clicked.
HandlerMethod
¶
HandlerMethod(
func: Callable[..., Awaitable[None]],
*,
handler_id: str,
version: int,
defer: AutoDefer | None = None,
)
A view method made a handler by @risa.handler.
Accessed on a view instance, as self.vote, it is the handler to put on a component,
bare or through risa.bind. It cannot be called. Accessed on the class, as
Poll.vote, it is this object, and Poll.vote.func is the plain function, for one
handler that delegates to another.
signature
property
¶
signature: HandlerSignature
The handler's wire parameters, resolved on first use.
View
¶
Bases: Struct
Base class of a view: a message's layout, its state, and the handlers behind it.
A view is a msgspec.Struct, and its annotated fields are its durable state, carried
from click to click. Mark a field risa.Prop[...] to rebuild it fresh on every click
instead, typically in load. Register the class with @risa.register, return its
layout from render, and send it with client.build:
@risa.register(name="counter")
class Counter(risa.View):
count: int = 0
def render(self) -> ui.Layout:
return ui.Row(ui.Button(self.add, label=f"Clicks: {self.count}"))
@risa.handler
async def add(self, ctx: risa.ComponentContext) -> None:
self.count += 1
await ctx.rerender()
Each click runs its handler on the latest saved state of its view, and clicks on one
view queue up within a process, each waiting up to ten seconds for the lock; one that
waits longer is logged and goes unanswered. A view without durable fields has nothing to
protect and takes no lock, so its handlers may run at the same time. Across processes,
an InStore view on a shared store lets the first write win, while for an InMessage
view the last edit of the message wins. A change made without ctx.rerender() is saved
when the handler returns, but that save can only log a failure, so a handler that must
know calls ctx.rerender() itself, before any ctx.respond(). A handler that raises
keeps what it saved through rerender() or edit() before, and loses the rest.
load
async
¶
load() -> None
Fill the view's props before it renders.
risa awaits it before the first send and on every click, after restoring the durable
state and before the handler runs. Its parameters are injected, as a handler's are.
An override of another view's load that adds parameters needs = risa.INJECTED on
them for a type checker, as any override that adds parameters would.
on_outdated
async
classmethod
¶
on_outdated(ctx: ComponentContext) -> None
Answer a click on a component this view can no longer route.
The component's handler was retired by a version bump, its wire parameters
changed without one, it carries an enum value the enum no longer has or fewer
arguments than the handler now takes, or its state was written by durable fields
the view no longer has. The default does nothing, so the click fails on the user's
side, and risa logs it. Override it to answer the user, or to replace the dead
message with ctx.edit(...) or ctx.replace(...). Its parameters are injected, as
a handler's are, and ctx has no view state.
To answer clicks on one retired handler differently, keep a method with its id, version and wire parameters whose body says the component is outdated. It routes like any handler, and nothing is logged.
Parameters:
-
ctx(ComponentContext) –The context of the click, which has no view state.
on_state_missing
async
classmethod
¶
on_state_missing(ctx: ComponentContext) -> None
Answer a click on an InStore view whose state is gone.
The record expired or was deleted. The default does nothing, so the click fails on
the user's side, and risa logs a warning. Override it to answer the user, or to
replace the message with ctx.edit(...). Its parameters are injected, as a
handler's are, and ctx has no view state.
Parameters:
-
ctx(ComponentContext) –The context of the click, which has no view state.
handler
¶
handler(func: HandlerFunction[V, P]) -> HandlerMethod[V, P]
Make a view method a handler that components can route to.
Used bare it returns the handler, and with options a decorator that makes one. A handler
takes the view and a ComponentContext, then its wire parameters, then anything to inject:
@risa.handler(defer=risa.AutoDefer.UPDATE)
async def vote(
self, ctx: risa.ComponentContext, option: int, db: Database
) -> None: ...
Wire parameters come first, are positional, and are annotated bool, int, str,
hikari.Snowflake or an enum.Enum of int or str values. They receive the
values risa.bind put on the component. Every parameter after them without a default
is injected through linkd, so a hikari enum such as hikari.ChannelType, which is not
an enum.Enum, is injected too; to carry one on the wire, bind its value as an int.
To inject an int or another of those types, default it to risa.INJECTED. A
handler returns None.
Parameters:
-
func(Callable[..., Awaitable[None]] | None, default:None) –The view method to make a handler. Left out, the call takes options and returns the decorator.
-
handler_id(str | None, default:None) –The id the handler routes under, the method's name by default. Pin it before renaming the method, so components already sent keep working.
-
version(int, default:1) –Bump it when the wire parameters change. Components sent before keep routing to a method that still has the old version; once none does, they go to
View.on_outdated. -
defer(AutoDefer | None, default:None) –The handler's
AutoDefer, overriding the view's.
register
¶
register(
*,
name: str,
version: int = 1,
defer: AutoDefer | None = None,
state: StatePlacement = DEFAULT_PLACEMENT,
) -> Callable[[type[T]], type[T]]
Register a view class so its components can be routed.
@risa.register(name="poll", state=risa.InStore(ttl=30 * 86400))
class Poll(risa.View): ...
A client then routes it once it is passed to client.add_view, or without that if the
client was made with use_global=True. The decorator raises ViewDeclarationError for a
class risa cannot serve, such as a blank name, a prop without a default, or an InStore
view without durable fields.
Parameters:
-
name(str) –The name the view routes under. Renaming it retires every component already sent.
-
version(int, default:1) –The schema version of the durable state. Appending fields that have defaults needs no bump; any other change to the durable fields does, and components sent before the bump no longer route to the view.
-
defer(AutoDefer | None, default:None) –The view's
AutoDefer, for handlers that set none. -
state(StatePlacement, default:DEFAULT_PLACEMENT) –Where the durable state lives:
InMessage(), the default, orInStore(...).
Returns: