posit-dev / posit-dev/py-shiny
Proposal: `input_*` functions take a handle as a parameter, or return a handle
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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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