posit-dev / py-shiny

Shiny for Python
https://shiny.posit.co/py/
MIT License
1.33k stars 82 forks source link

How to properly type annotate reactive objects #1676

Open ltuijnder opened 2 months ago

ltuijnder commented 2 months ago

I have some trouble with properly type hinting reactive objects when working with modules servers.

I use pyright as my LSP and I want to properly type annotate the return types of module server objects (that are reactive) such that in other code the static type checking is correct.

Type hinting reactive Calc:

Say I have reactive calc that returns an object of type 'T'. How do I type annotate the output of my module server in the following situation.

from shiny import module, reactive

@module.server
def mod_server(input, output, session) -> ???:
    @reactive.calc
    def my_calc() -> T:
         return object_of_type_T
    return my_calc

My pyright LSP is saying that the return type of modserver is of type `Calc[T]. But I see thatCalc_` is explicitly listed as not exported over here: https://github.com/posit-dev/py-shiny/blob/main/shiny/reactive/__init__.py#L16

I could define type my own generic type that represent a ReactiveCalc function, ~but then pyright would complain about mismatch between listed return type and the Calc_[T] return.~

Am I missing something?

Type hinting input.xxx

Secondly, how to properly annotate input.xxx reactives? Such that my Lsp would display the correct type for: input.xxx().

Eg. how would I type annotate the return of the following module server? (inspired by the module communcation article )

from shiny import module, render, ui

@module.ui
def city_state_ui():
    return ui.input_selectize("state", "State", choices=["NY", "CO", "OR", "MI"], selected="NY")

@module.server 
def mod_server(input, output, session) -> ???:
    return input.state

Reactive value

Type hinting with reactive value objects seems to be supported. At least the following type hinting results in expected behaviour:

from shiny import reactive
from shiny.reactive import Value 
x : Value[int] = reactive.value(1)
ltuijnder commented 2 months ago

So it turns out that pyright does not complain when you define your own generic RCalc[T] and use it as its return type.

from shiny import module, reactive
from typing import TypeVar, Callable

# RCalc = Generic for a callable with nor arguments that returns type T.
U = TypeVar(U)
type RCalc[U] = Callable[[], U]

@module.server
def mod_server(input, output, session) -> RCalc[T]:
    @reactive.calc
    def my_calc() -> T:
         return object_of_type_T
    return my_calc

Not sure if this should get standardized. Eg. maybe a shiny.typing module could define something like the above generic. or added to (or in types.py) Such that everyone would use the same generic. Or else each project needs to define a 'utils' RCalc generic.

Or maybe better allow RCalc_ to be used. Because if in the future if reactives calc gets extended with useful methods then they would be automatically available to users that used RCalc_ as the type hint.


Then type hinting for input.xxx turns out to be more difficult. As the type of your value depends on the input widgets which you need to match with the id. And some input widgets can return different types. https://github.com/posit-dev/py-shiny/issues/70 highlights the issue (and proposes a solution).

It seems the best that you can do is to type hint the object input.x or input['x'] directly yourself directly with reactive.Value[T] (where T is your type that gets returned by the widget).

Though less nice is that from the type hint you cannot directly distinguish it between a reactive.Value (that is also writable) vs a read only reactive.Value like inputs.