Skip to content

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() or ctx.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.

defer property

defer: AutoDefer | None

The handler's own AutoDefer, or None to use the view's.

func property

func: Callable[..., Awaitable[None]]

The decorated function.

handler_id property

handler_id: str

The id the handler routes under.

owner property

owner: type[View] | None

The view class the handler is defined on.

signature property

signature: HandlerSignature

The handler's wire parameters, resolved on first use.

token property

token: str

The routing token, a hash of the id and the version.

version property

version: int

The handler's version.

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:

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:

render

render() -> Layout

Return the view's layout, drawn from its current state.

It runs for the first send and for every rerender. Keep it synchronous and free of I/O; fetch what it shows in load.

handler

handler(func: HandlerFunction[V, P]) -> HandlerMethod[V, P]
handler(
    *,
    handler_id: str | None = ...,
    version: int = ...,
    defer: AutoDefer | None = ...,
) -> _HandlerDecorator

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, or InStore(...).

Returns:

  • Callable[[type[T]], type[T]] –

    The decorator. It returns the class unchanged.