posit-dev / posit-dev/py-shiny

Proposal: `input_*` functions take a handle as a parameter, or return a handle

Open
#70 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
1.8k
Forks
135
Avg merge
2d 18h
Merged PRs (30d)
21

Description

Currently, we match up input UI components with the actual input value by using the same strings. For example, these go together:

# UI component
input_text("txt", "Text input:")

# Value in server function
input.txt()

However, the Python type checker has no idea that these two things are related. In order to specify types for input.txt(), we need to do this in addition to the code above:

class MyInputs(Inputs):
     txt: reactive.Value[Union[str, None]]

def server(input: Inputs, output: Outputs, session: Session):
    input = cast(MyInputs, input)

As the user modifies code, it's possible that these type definitions will get out of sync, or that there will be unused definitions (like if txt is removed from the app).

Here are some possible ways to make the type checker help out with checking input values.

Option 1: modify input_* functions to take an InputHandle parameter

In the example below, the input_num() function takes an InputHandle() object as a parameter.

from typing import Generic
from htmltools import *
from shiny import *

# ==========================================
# Stuff that goes in Shiny
# ==========================================
class InputHandle(Generic[T]):
    def __init__(self, id: str):
        self._id = id

    def __call__(self) -> T:
        s = session.get_current_session()
        if s is None:
            raise RuntimeError("No session is active.")
        x = s.input[self._id]()
        return x

# Define inputs like this
def input_num(input: InputHandle[float], label: str, value: float) -> Tag:
    return ui.input_numeric(input._id, label, value)


# ==========================================
# Usage in an app
# ==========================================
# The input ID will be "n"
input_n = InputHandle[float]("n")

app_ui = ui.page_fluid(
    input_num(input_n, "Enter N:", 123),
    ui.output_text_verbatim("txt")
)

def server(input: Inputs, output: Outputs, session: Session):
    @output()
    @render_text()
    def txt():
        return f"n*2 is {input_n() * 2}"

app = App(app_ui, server)

This will allow the type checker to make sure that the value of input_n() is the same type that an input_num() would provide.

One drawback here is that input_n still needs to be explicitly defined.

Option 2: modify input_* functions to return an InputHandle

The walrus operator (:=) makes it possible to do assignment while passing a value to a function. For example, if our input_num returned an InputHandle object (defined slightly differently from the previous example), that InputHandle could be Tagifiable, so it could be inserted directly into the UI, and we could use that InputHandle in the server code to get the values.

from typing import Generic
import htmltools
from shiny import *

# ==========================================
# Stuff that goes in Shiny
# ==========================================
class InputHandle(Generic[T]):
    def __init__(self, id: str, ui: htmltools.core.Tagifiable):
        self._id = id
        self._ui = ui

    def __call__(self) -> T:
        s = session.get_current_session()
        if s is None:
            raise RuntimeError("No session is active.")
        x = s.input[self._id]()
        return x

    def tagify(self) -> TagChildArg:
        return self._ui.tagify()

def input_num(id: str, label: TagChildArg, value: float) -> InputHandle[float]:
    ui_ = ui.input_numeric(id, label, value)
    return InputHandle[float](id, ui_)

# ==========================================
# Usage in an app
# ==========================================
app_ui = ui.page_fluid(
    input_n := input_num("n", "N", 20),
    ui.output_text_verbatim("txt")
)

def server(input: Inputs, output: Outputs, session: Session):
    @output()
    @render_text()
    def txt():
        return f"n*2 is {input_n() * 2}"

app = App(app_ui, server)

It may even be possible to modify input_num() so that we don't even need to pass an ID like "n" -- maybe it could be autogenerated?

This method is very concise and also makes it possible to use the type checker. The one drawback I can see is that the inline definition with := somewhat obscures the fact that a variable is being defined. Also, := may be unfamiliar to many users, although that will likely change over time.

A similar method that doesn't make use of := is maybe something like the code below. However, I'm not yet certain we can make the type checker correctly infer types from this:

input = MyInputs()

app_ui = ui.page_fluid(
    input.n(input_num("n", "N", 20)),
)

Open questions:

  • Can we make outputs be automatically typed in a similar way?

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

No repository files, tests, or entry points are named. Start by evaluating the two proposed InputHandle designs and the open question about automatically typed outputs; done means selecting and implementing a concrete approach that lets the type checker link UI inputs with server values without manually duplicated definitions.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
developer-experience, frontend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.