Giter Club home page Giter Club logo

py-shiny's Introduction

Shiny for Python

PyPI Latest Release Build status Conda Latest Release Supported Python versions License

Shiny for Python is the best way to build fast, beautiful web applications in Python. You can build quickly with Shiny and create simple interactive visualizations and prototype applications in an afternoon. But unlike other frameworks targeted at data scientists, Shiny does not limit your app's growth. Shiny remains extensible enough to power large, mission-critical applications.

To learn more about Shiny see the Shiny for Python website. If you're new to the framework we recommend these resources:

Join the conversation

If you have questions about Shiny for Python, or want to help us decide what to work on next, join us on Discord.

Getting started

To get started with shiny follow the installation instructions or just install it from pip.

pip install shiny

To install the latest development version:

# First install htmltools, then shiny
pip install https://github.com/posit-dev/py-htmltools/tarball/main
pip install https://github.com/posit-dev/py-shiny/tarball/main

You can create and run your first application with shiny create, the CLI will ask you which template you would like to use. You can either run the app with the Shiny extension, or call shiny run app.py --reload --launch-browser.

Development

If you want to do development on Shiny for Python:

pip install -e ".[dev,test]"

Additionally, you can install pre-commit hooks which will automatically reformat and lint the code when you make a commit:

pre-commit install

# To disable:
# pre-commit uninstall

py-shiny's People

Contributors

adejumoridwan avatar cclauss avatar cpsievert avatar dianaclarke avatar fpgmaas avatar gadenbuie avatar gregswinehart avatar gshotwell avatar has2k1 avatar jcheng5 avatar joesho112358 avatar jonmoore avatar karangattu avatar lachlansimpson avatar nealrichardson avatar nstrayer avatar pierre-bartet avatar schackartk avatar schloerke avatar skaltman avatar wch avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

py-shiny's Issues

Should have simple way to declare types of input vars

Currently it's possible to declare the types of input values like this:

class MyInputs(Inputs):
    txt: reactive.Value[str]
    go: reactive.Value[int]

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

But it would be nice to be able to do something like this:

class MyInputs(Inputs):
    txt: str
    go: int

Notes:

  • Some inputs (like those from dynamic UI) will have a possible value of None
  • This should inform Python that both input.txt and input["txt"] are of type reactive.Value[str].

I think it should be possible to do these things. TypedDict, for example, does some complex stuff with types:
https://github.com/python/cpython/blob/bd8b05395ad8877f4a065562444e117b1650c022/Lib/typing.py#L2381-L2438

Shiny on pyodide no longer works

I've bisected this to 4a0e5bf, from #39.

It appears to not send any values to the client. It still sends messages about recalculating, though. This is a screenshot of the behavior with the basic app. It should show a verbatim text output that says n*2 is 40, but it shows nothing.

image

get_current_session() doesn't pick up on a ModuleSession correctly

This should print a class of ModuleSession, not Session:

from typing import Callable
from shiny import *
from shiny.session import get_current_session
from shiny.modules import *


def my_ui(ns: Callable[[str], str]) -> ui.Tag:
    return ui.output_text(ns("session_info"))


def my_server(input: ModuleInputs, output: ModuleOutputs, session: ModuleSession):
    @output()
    @render_text()
    def session_info() -> str:
        return str(type(get_current_session()))


my_module = Module(my_ui, my_server)

app_ui = ui.page_fluid(my_module.ui("foo"))

def server(input: Inputs, output: Outputs, session: Session):
    my_module.server("foo")


app = App(app_ui, server)

Should figure out how to check sync/async types for decorators

In the example below, the last function, gas, should be flagged, since a synchronous function can't depend on an async event. Currently, our typing does not flag it. (I don't know that it's actually possible with Python.)

def fs() -> int:
    return 1

async def fa() -> int:
    return 1

# sync-sync - OK
@event(fs)
def gss() -> int:
    return 3

# async-async - OK
@event(fa)
async def gaa() -> int:
    return 3

# sync-async - OK
@event(fs)
async def gsa() -> int:
    return 3

# async-sync - should be flagged
@event(fa)
def gas() -> int:
    return 3

`navs_pill_list` isn't implemented (yet)

This should work but it currently doesn't

from shiny import *

app_ui = ui.page_fluid(
    ui.navs_pill_list(
        ui.nav("a", "tab a content"),
        ui.nav("b", "tab a content"),
    )
)

def server(input: Inputs, output: Outputs, session: Session):
    pass

app = App(app_ui, server)

Outside of app, errors in observers disappear

For example, with this code, nothing shows in the terminal. It should at least show a warning.

import asyncio
from shiny import *
import shiny.reactcore

@observe()
def _():
    raise Exception("Error here!")

asyncio.run(shiny.reactcore.flush())

If an observer error occurs in a the server function of Shiny app, then the app crashes, which is good. However, if the observer error occurs in a Shiny app outside of the server function, then nothing happens.

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

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?

Errors when cancelling callbacks

I thought of this because it's similar to #26. If a callback is cancelled during the invocation of callbacks, an error can occur.

from shiny.utils import Callbacks
cb = Callbacks()

cb.register(lambda: cancel())

cancel = cb.register(lambda: print("this is a callback"))
cb.invoke()
#> KeyError: '2'

A different (and simpler) case: if a callback's cancel function is called after the callback has been invoked, it throws an error:

from shiny.utils import Callbacks
cb = Callbacks()
cancel = cb.register(lambda: print("callback 1"), once=True)
cb.invoke()

cancel()
#> KeyError: '1'

Module() server functions don't return a value when invoked

For example, this should print "return value"

from typing import Callable
from shiny import *
from shiny.modules import *


def my_ui(ns: Callable[[str], str]) -> ui.Tag:
    return ui.input_action_button(ns("btn"), "Press")


def my_server(input: ModuleInputs, output: ModuleOutputs, session: ModuleSession):
    @reactive.Effect()
    def _():
        print(input.btn())

    return "return value"


my_module = Module(my_ui, my_server)

app_ui = ui.page_fluid(my_module.ui("foo"))


def server(input: Inputs, output: Outputs, session: Session):
    val = my_module.server("foo")
    print(val)


app = App(app_ui, server)

When `@event(ignore_none=True, ignore_init=True)` is used, should the initial `None` count as an initial value?

When @event() is used with both ignore_none=True (the default) and ignore_init=True (not default), a starting None value due to an unpopulated input can count as the initial value. This can happen when an input is part of dynamic UI.

This example app illustrates how static and dynamic UI behaves differently.

from shiny import *

app_ui = ui.page_fluid(
    ui.input_text("static_txt", "Static text input", "starting value"),
    ui.tags.p("@event()"),
    ui.output_text_verbatim("static_txt_value", placeholder=True),
    ui.tags.p("@event(ignore_init=True)"),
    ui.output_text_verbatim("static_txt_noinit_value", placeholder=True),
    ui.tags.hr(),
    ui.output_ui("dyn_ui"),
    ui.tags.p("@event()"),
    ui.output_text_verbatim("dyn_txt_value", placeholder=True),
    ui.tags.p("@event(ignore_init=True)"),
    ui.output_text_verbatim("dyn_txt_noinit_value", placeholder=True),
)


def server(input: Inputs, output: Outputs, session: Session):
    @output()
    @render_text()
    @event(input.static_txt)
    def static_txt_value():
        return str(input.static_txt())

    @output()
    @render_text()
    @event(input.static_txt, ignore_init=True)
    def static_txt_noinit_value():
        return str(input.static_txt())

    @output()
    @render_text()
    @event(input.dyn_txt)
    def dyn_txt_value():
        return str(input.dyn_txt())

    @output()
    @render_text()
    @event(input.dyn_txt, ignore_init=True)
    def dyn_txt_noinit_value():
        return str(input.dyn_txt())

    # UI component for dynamic text input
    @output()
    @render_ui()
    def dyn_ui():
        return ui.input_text("dyn_txt", "Dynamic input text", "starting value")


app = App(app_ui, server, debug=True)

In the screenshot below, with @event(ignore_init=True), the static case shows nothing, but the dynamic case shows starting value. This is because, for the dynamic UI case, when unpopulated, input.dyn_txt() returns None, and that counts as an initial value.

image

This difference seems strange to me, especially if we specify types for input values, with something roughly like:

input.dyn_txt: reactiveValue[str]

Consider swapping order of choices and values for `input_radio_buttons`, `input_checkbox_group`, and `input_select`

For input_radio_buttons, input_checkbox_group, and input_select, the choices are specified as {"Label": "value"}, like this:

input_select("x", "Label", {"Choice A": "a", "Choice B": "b"})

This is a direct translation of how it's done in R. I think we should consider swapping the order, like this:

input_select("x", "Label", {"a": "Choice A", "b": "Choice B"})

This will allow us to reduce remove the choice_values and choice_names parameters.


For input_radio_buttons and input_checkbox_group, the choice_values and choice_names parameters let you pass in the values and labels separately.

input_checkbox_group("x", "Label", choice_values=["a", "b"], choice_names=["Choice A", "Choice B"])

This comes directly from the R versions of these functions.

checkboxGroupInput("x", "Label", choiceValues=c("a", "b"), choiceNames=c("Choice A", "Choice B"))

According to the R documentation, the purpose of the separate parameters is:

choiceNames, choiceValues: List of names and values, respectively, that are displayed to the user in the app and correspond to the each choice (for this reason,ย choiceNamesย andย choiceValuesย must have the same length). If either of these arguments is provided, then the otherย mustย be provided andย choicesย must notย be provided. The advantage of using both of these over a named list forย choicesย is thatย choiceNamesย allows any type of UI object to be passed through (tag objects, icons, HTML code, ...), instead of just simple text. See Examples.

So in R, it lets you do this:

checkboxGroupInput(
  "x",
  "Label",
  choiceValues=c("a", "b"), 
  choiceNames=list(tags$b("Choice A"), tags$i("Choice B"))
)

The reason that separate choiceValues and choiceNames params are needed is because you can't do list(tags$b("Choice A")="a"), where a complex object is the name of an element in the list:

checkboxGroupInput(
  "x",
  "Label",
  choices=list(tags$b("Choice A")="a", tags$i("Choice B")="b")
)

IIRC, the reason for those extra parameters is because we had already established the order as list("Choice A"="a"), and then only later on did we consider the need for passing in complex objects as labels: rstudio/shiny#1521


For prism, we have the opportunity to swap the label and value, so users could just do this. This order also makes a bit more sense to me anyway:

input_checkbox_group("x", "Label", choices={"a": tags.b("Choice A"), "b": tags.i("Choice B")})

The other order (the order we currently use) doesn't work, because complex objects can't be used as keys:

input_checkbox_group("x", "Label", choices={tags.b("Choice A"): "a", tags.i("Choice B"): "b"})
#> TypeError: unhashable type: 'Tag'

This change would eliminate two parameters (choice_names and choice_values), make usage more consistent, and the value: label ordering just makes more sense. The only drawback that I can see is that the order is different from what we use in R.

Module() server function arguments aren't accessible to debugger

If you insert a breakpoint() inside this function

https://github.com/rstudio/prism/blob/7cd15d60b5eb1c6f70384b5d68eacec7cae34a69/examples/moduleapp/app.py#L30-L31

and run the app: shiny run examples.moduleapp.app.

You'll see:

(Pdb) input
<built-in function input>
(Pdb) output
*** NameError: name 'output' is not defined
(Pdb) session
<module 'shiny.session' from './shiny/session/__init__.py'>

(Obviously, print() the arguments it gives you the right results...)

`input_*` functions crash when running in pyodide

The input_* functions work when running shiny normally, but when running pyodide they error out. This probably has something to do with the websocket emulation layer we use in pyodide. The message is Unhandled error: async function yielded control; it did not finish in one iteration, which an error message from run_coro_sync(). This is the function that runs async functions from synchronous ones, but expects the async function to not actually yield.

image

App code:

from shiny import *

ui = page_fluid(
    input_text("x", "X"),
    input_action_button("update_x", "Update X"),
)

def server(s: ShinySession):
    @observe()
    def _():
        if not s.input["update_x"]:
            return
        update_text("x", value = "New value")

app = ShinyApp(ui, server, debug=True)

Methods which call `session.send_message()` crash app on pyodide

On pyodide, some functions which call session.send_message() (like ui_insert()) cause the app to crash with this message:

Unhandled error: async function yielded control; it did not finish in one iteration.

The problem seems to be that session.send_message() is wrapped in run_coro_sync(). This can be seen by adding utils.run_coro_sync(s.send_message(...)) directly to an app:

from shiny import *

ui = page_fluid(
    input_action_button("go", "Send message to client"),
)

def server(s: ShinySession):
    @observe()
    def _():
        if not s.input["go"]:
            return
        utils.run_coro_sync(s.send_message({"foo": "1"}))

app = ShinyApp(ui, server, debug=True)

This works fine when running the app normally (not in pyodide).

`shiny` script prints unhelpful message when import fails

If you add this to the top of examples/simple/app.py:

import asdf

Then launch with:

shiny run examples/simple/app.py

It will print this error message, which is confusing:

$ shiny run examples/simple/app.py
Traceback (most recent call last):
  File "/Users/winston/bin/shiny", line 33, in <module>
    sys.exit(load_entry_point('shiny', 'console_scripts', 'shiny')())
  File "/opt/homebrew/lib/python3.9/site-packages/click/core.py", line 1128, in __call__
    return self.main(*args, **kwargs)
  File "/opt/homebrew/lib/python3.9/site-packages/click/core.py", line 1053, in main
    rv = self.invoke(ctx)
  File "/opt/homebrew/lib/python3.9/site-packages/click/core.py", line 1659, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/opt/homebrew/lib/python3.9/site-packages/click/core.py", line 1395, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/opt/homebrew/lib/python3.9/site-packages/click/core.py", line 754, in invoke
    return __callback(*args, **kwargs)
  File "/Users/winston/Dropbox/Projects/prism/shiny/_main.py", line 98, in run
    app = resolve_app(app, app_dir)
  File "/Users/winston/Dropbox/Projects/prism/shiny/_main.py", line 155, in resolve_app
    raise ImportError(f"Could not find the module '{module}'")
ImportError: Could not find the module 'examples.simple.app'

Server should send `progress` and `busy` messages to client

Currently, pyshiny does not send busy and idle messages to the client.

Here's a log of the communication in R-shiny:

SEND {"config":{"workerId":"","sessionId":"da408435a4b00fe1731b0926eda7bc49","user":null}}
RECV {"method":"init","data":{"n:shiny.number":1,".clientdata_output_txt_hidden":false,".clientdata_pixelratio":2,".clientdata_url_protocol":"http:",".clientdata_url_hostname":"127.0.0.1",".clientdata_url_port":"7453",".clientdata_url_pathname":"/",".clientdata_url_search":"",".clientdata_url_hash_initial":"",".clientdata_url_hash":"",".clientdata_singletons":""}}
SEND {"busy":"busy"}
SEND {"recalculating":{"name":"txt","status":"recalculating"}}
SEND {"recalculating":{"name":"txt","status":"recalculated"}}
SEND {"busy":"idle"}
SEND {"errors":{},"values":{"txt":"input$n is 1"},"inputMessages":[]}
RECV {"method":"update","data":{"n:shiny.number":5}}
SEND {"progress":{"type":"binding","message":{"id":"txt"}}}
SEND {"busy":"busy"}
SEND {"recalculating":{"name":"txt","status":"recalculating"}}
SEND {"recalculating":{"name":"txt","status":"recalculated"}}
SEND {"busy":"idle"}
SEND {"errors":{},"values":{"txt":"input$n is 5"},"inputMessages":[]}

And a similar one in py-shiny:

SEND: {"config": {"workerId": "", "sessionId": "1", "user": null}}
RECV: {"method":"init","data":{"n":20,".clientdata_output_txt_hidden":false,
".clientdata_pixelratio":2,".clientdata_url_protocol":"https:",".clientdata_
url_hostname":"rstudio.github.io",".clientdata_url_port":"",".clientdata_url
_pathname":"/prism-experiments/app-b5nq8bxz2uydkuf2y7wk/",".clientdata_url_s
earch":"",".clientdata_url_hash_initial":"",".clientdata_url_hash":"",".clie
ntdata_singletons":""}}
SEND: {"recalculating": {"name": "txt", "status": "recalculating"}}
SEND: {"recalculating": {"name": "txt", "status": "recalculated"}}
SEND: {"errors": {}, "values": {"txt": "n*2 is 40"}, "inputMessages": []}
RECV: {"method":"update","data":{"n":49}}
SEND: {"recalculating": {"name": "txt", "status": "recalculating"}}
SEND: {"recalculating": {"name": "txt", "status": "recalculated"}}
SEND: {"errors": {}, "values": {"txt": "n*2 is 98"}, "inputMessages": []}

Proposal: `input.x()` raise `SilentException` instead of `None` when value is not yet populated

tl;dr: req() is supposed to be for validation and @event() is supposed to be for controlling reactivity, but it turns out that in some use cases @event() actually does a better job of validation than req(), which would drive users (like me) to use @event() when it's not quite appropriate. I propose we make input.x() throw a SilentException when the value hasn't been populated yet.

I've found myself using the following two constructs in my apps, and I think it will be a bit confusing for users to understand which one is appropriate for which uses cases:

A. @event(input.x)
B. req(input.x())

These have two different purposes:

  • @event(): Control the triggering of reactivity
  • req(): Validate input values.

However, it's easy to mix up the use of @event() and req(), because they overlap quite a bit in their behavior.

Here are some use cases. The first two involve controlling reactivity; the remaining three involve validation.

  1. Run the code when the user clicks on a button. This works with @event() and req().
    • Note: it works with @event(input.x) because we have special-cased the handling of the action button value 0. If we didn't do that, users would have to write @event(input.x, ignore_init=True)
  2. Run the code when an input changes. This can be handled by @event(). req() does not work well because falsiness in Python is too broad, encompassing [], "", 0, and so on.
  3. Is input.x() a value which will not cause an error? Usually, this means that the value is not None. @event() works here; req() is too broad.
  4. Is input.x() a useful (non-falsy) value? req() usually works here, but not always.

In case 3 above, @event() is actually more useful for validating inputs than req(), but that's not exactly the purpose of @event(); using it that way would cause an Effect or output to not depend on other reactive inputs.

Generally speaking, there's three kinds of validation that we're interested in:

  • Is the value populated at all?
  • Is the value truthy?
  • Does the value satisfy some custom condition, like input.x() > 10?

Currently, we're expecting req() to do the lifting for all of these. It only does the second one natively; the first and third require extra logic, like if input.x() is None: req(False).


Proposal:

  • If input.x() is called before it has been populated, it throws a SilentException.
    • Could add an option like input.x(throw=False), which causes it to return None instead of raising an exception.
  • Add a method input.x.ready(), which returns False until the value has been populated from the client; after that it returns True.

Then we could simplify code like this:

@output()
@render_text()
def txt():
    if input.n() is None:
       req(False)
    return f"n*2 is {input.n() * 2}"

Into this:

@output()
@render_text()
def txt():
    return f"n*2 is {input.n() * 2}"

If users want a None for not-yet-populated values, then they can check input.x.ready().

This doesn't remove the need for the third kind of validation, as in if input.x() <= 10: req(False), but it's not possible to entirely get away from writing custom logic there.

Another advantage of having input.n() throw an exception if not yet ready is that the type can be specified more precisely. Presently, you can specify types like this:

class MyInputs(Inputs):
    n: reactive.Value[Union[int, None]]

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

The None is necessary because it is a possible value returned by input.n(). But if it were to throw an exception when not ready, then we can be sure that it's always an int, which allows us to simplify it to:

class MyInputs(Inputs):
    n: reactive.Value[int]

ModuleSession doesn't inherit all attributes from Session

For example I'd expect this to return True but it returns False

from typing import Callable
from shiny import *
from shiny.modules import *


def my_ui(ns: Callable[[str], str]) -> ui.Tag:
    return ui.output_ui(ns("session_info"))


def my_server(input: ModuleInputs, output: ModuleOutputs, session: ModuleSession):
    @output()
    @render_ui()
    def session_info() -> bool:
        return hasattr(session, "app")


my_module = Module(my_ui, my_server)

app_ui = ui.page_fluid(my_module.ui("foo"))


def server(input: Inputs, output: Outputs, session: Session):
    my_module.server("foo")


app = App(app_ui, server)

Figure out why tests fail in GHA when run from top level

In GitHub Actions, if make test or pytest is run from the top level, the tests fail to run. For example:
https://github.com/rstudio/prism/runs/4051096368?check_suite_focus=true#step:10:16

However, if it cds to the tests/ directory before running pytest, it works. (make test won't work from that dir because the Makefile isn't there.)
https://github.com/rstudio/prism/blob/03b9d80e7e4feee324b859b51ac23313e087133e/.github/workflows/pytest.yaml#L59-L60

When I run make test or pytest from the top level locally, they work fine; there's something different about the GHA setup that makes it fail.

In the py-htmltools repo, running tests from the top level works without going into the tests/ dir:
https://github.com/rstudio/py-htmltools/blob/c7da5c18084e6919ae69b41fe63d1703141c5baa/.github/workflows/pytest.yaml#L36

Need to define `__all__` for each .py file

We have files that import objects from other modules/packages but do not have an __all__ specified. In those cases, the imported objects automatically get re-exported, so you can do things like this:

import shiny
shiny.warn
#> <built-in function warn>
shiny.Any
#> typing.Dict

I believe the solution is to define __all__ for all .py files that we have.

Setting `ReactiveVal` leads to `Unhandled error` in some cases

The following app has a single button, which, when clicked, results in an Unhandled error.

from shiny import *


ui = page_fluid(
    input_action_button("go", "Go"),
)

def server(session: ShinySession):
    v = ReactiveVal(0)

    @reactive()
    def r():
        print("in reactive")
        return v()

    @observe()
    def _():
        session.input["go"]   # Take a dependency on the go button

        with isolate():
            print("in observer 1-1")
            r()
            print("in observer 1-2")
            val = v()
            print("v() is " + str(val))
            print("in observer 1-3")
            v(val + 1)
            print("in observer 1-4")


    @observe()
    def _():
        print("in observer 2-1")
        r()
        print("in observer 2-2")

app = ShinyApp(ui, server)

if __name__ == "__main__":
    app.run()

Before clicking the button, it prints this output:

in observer 1-1
in reactive
in observer 1-2
v() is 0
in observer 1-3
in observer 1-4
in observer 2-1
in reactive
in observer 2-2

When the button is clicked, this output shows up:

in observer 1-1
in observer 1-2
v() is 1
in observer 1-3
Unhandled error: 2763

So the error is happening when v(val + 1) is called.

Almost any change to the structure seems to make the error stop happening. For example, removing the first call to r() makes the error go away.

Single `nav` in `nav_tabs_card` results in no output

This app results in a blank page:

from shiny import *

app_ui = ui.page_fluid(
    ui.navs_tab_card(
        ui.nav(
            "Hello"
        ),
    ),
)

def server(input: Inputs, output: Outputs, session: Session):
    pass

app = App(app_ui, server)

However, adding a second nav makes it so both are visible:

from shiny import *

app_ui = ui.page_fluid(
    ui.navs_tab_card(
        ui.nav(
            "Hello"
        ),
        ui.nav(
            "there"
        ),
    ),
)

def server(input: Inputs, output: Outputs, session: Session):
    pass


app = App(app_ui, server)

image

Loading shiny fails when installed normally

When shiny is installed normally (not pip install -e), loading it causes an error.

To install it:

pip3 uninstall shiny
make install

After installing it, loading results in an error:

>>> import shiny
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/opt/homebrew/lib/python3.9/site-packages/shiny-0.0.0.9001-py3.9.egg/shiny/__init__.py", line 9, in <module>
    from . import ui
  File "/opt/homebrew/lib/python3.9/site-packages/shiny-0.0.0.9001-py3.9.egg/shiny/ui/__init__.py", line 7, in <module>
    from ._bootstrap import *
  File "/opt/homebrew/lib/python3.9/site-packages/shiny-0.0.0.9001-py3.9.egg/shiny/ui/_bootstrap.py", line 40, in <module>
    from ._page import get_window_title
  File "/opt/homebrew/lib/python3.9/site-packages/shiny-0.0.0.9001-py3.9.egg/shiny/ui/_page.py", line 29, in <module>
    from ._navs import navs_bar
  File "/opt/homebrew/lib/python3.9/site-packages/shiny-0.0.0.9001-py3.9.egg/shiny/ui/_navs.py", line 529, in <module>
    def navs_hidden(
  File "/opt/homebrew/lib/python3.9/site-packages/shiny-0.0.0.9001-py3.9.egg/shiny/_docstring.py", line 15, in _
    raise ValueError(f"No example for {fn_name}")
ValueError: No example for navs_hidden

`input_date()` should allow strings as input

Currently input_date() takes date objects as input. This is a bit inconvenient if the user just wants to provide the date as an ISO-formatted string. They currently have to do something like this:

from datetime import date

input_date("x", "x", date.fromisoformat("2022-01-01"))

It would be nice to be able to do it this way:

input_date("x", "x", "2022-01-01")

And same for input_date_range(), and possibly input_slider().

navs don't work with @render_ui()

Likely because JSXTag() doesn't work with @render_ui() (likely because of how document.currentScript works)

from shiny import *

app_ui = ui.page_fluid(ui.output_ui("tabs"))

def server(input: Inputs, output: Outputs, session: Session):
    @output()
    @render_ui()
    def tabs():
        return ui.navs_tab(ui.nav("a", "tab a content"), ui.nav("b", "tab b content"))

app = App(app_ui, server)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.