Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add shinylive url command #20

Merged
merged 41 commits into from
Jan 12, 2024
Merged

feat: Add shinylive url command #20

merged 41 commits into from
Jan 12, 2024

Conversation

gadenbuie
Copy link
Contributor

@gadenbuie gadenbuie commented Jan 4, 2024

Adds shinylive url, a command group to encode (or create) a shinylive.io URL for Python or R apps from local files or decode the existing URL. The core functionality is wrapped in encode_shinylive_url() and decode_shinylive_url() which are exported to facilitate creating and decoding shinylive URLs from within a Python session (the primary use case is for the Component and Layout galleries, but I'm sure this would be broadly useful).

❯ shinylive url encode --help
Usage: shinylive url encode [OPTIONS] APP [FILES]...

  Create a shinylive.io URL for a Shiny app from local files.

  APP is the path to the primary Shiny app file.

  FILES are additional supporting files or directories for the app.

  On macOS, you can copy the URL to the clipboard with:

      shinylive url encode app.py | pbcopy

Options:
  -m, --mode [editor|app]         The shinylive mode: include the editor or
                                  show only the app.  [required]
  -l, --language [python|py|R|r]  The primary language used to run the app, by
                                  default inferred from the app file.
  -v, --view                      Open the link in a browser.
  --no-header                     Hide the Shinylive header.
  --help                          Show this message and exit.
❯ shinylive url decode --help
Usage: shinylive url decode [OPTIONS] [URL]

  Decode a shinylive.io URL.

  URL is the shinylive editor or app URL. If not specified, the URL will be
  read from stdin, allowing you to read the URL from a file or the clipboard.

  On macOS, you can read the URL from the clipboard with:

      pbpaste | shinylive url decode

Options:
  --out TEXT  Output directory into which the app's files will be written. The
              directory is created if it does not exist.
  --json      Prints the decoded shinylive bundle as JSON to stdout, ignoring
              --out.
  --help      Show this message and exit.

Examples

Copy the multiple files Python app example link:

pbpaste | shinylive url decode
## file: app.py

from shiny import App, render, ui
from utils import square

app_ui = ui.page_fluid(
    ui.input_slider("n", "N", 0, 100, 20),
    ui.output_text_verbatim("txt"),
)


def server(input, output, session):
    @output
    @render.text
    def txt():
        val = square(input.n())
        return f"{input.n()} squared is {val}"


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

## file: utils.py

def square(n):
    return n * n

Copy this example Shiny app

from shiny import App, render, ui

app_ui = ui.page_fluid(
    ui.input_slider("x", "Slider input", min=0, max=20, value=10),
    ui.output_text_verbatim("txt"),
)


def server(input, output, session):
    @output
    @render.text
    def txt():
        return f"x: {input.x()}"


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

and then generate a link and open it on shinylive

pbpaste | shinylive url encode --view

Try the same idea, but with this R app

library(shiny)

ui <- fluidPage(
  h2(textOutput("currentTime"))
)

server <- function(input, output, session) {
  output$currentTime <- renderText({
    invalidateLater(1000, session)
    paste("The current time is", Sys.time())
  })
}

shinyApp(ui, server)
pbpaste | shinylive url encode --view

Or apply the same idea to a local app on your computer. In this case, the language is inferred from the file extension.

shinylive url encode -v posit-dev/py-shiny/examples/event/app.py

shinylive url encode -v rstudio/shiny/inst/examples/11_timer/app.R

shinylive/_url.py Outdated Show resolved Hide resolved
setup.cfg Show resolved Hide resolved
shinylive/_main.py Outdated Show resolved Hide resolved
shinylive/_main.py Outdated Show resolved Hide resolved
shinylive/_url.py Outdated Show resolved Hide resolved
shinylive/_main.py Outdated Show resolved Hide resolved
shinylive/_url.py Outdated Show resolved Hide resolved
shinylive/_main.py Outdated Show resolved Hide resolved
shinylive/_url.py Outdated Show resolved Hide resolved
shinylive/_url.py Outdated Show resolved Hide resolved
shinylive/_main.py Show resolved Hide resolved
shinylive/_main.py Outdated Show resolved Hide resolved
shinylive/_main.py Show resolved Hide resolved
shinylive/_main.py Outdated Show resolved Hide resolved
shinylive/_main.py Outdated Show resolved Hide resolved
@gadenbuie
Copy link
Contributor Author

gadenbuie commented Jan 6, 2024

Thank you for your comments and suggestions @wch, they were super helpful! I've addressed and resolved them all, except for two bigger items that I'll call out because they might be hard to find in the UI above.

  1. We've identified some weirdness around the dual-purpose app argument in encode_shinylive_url(). As it was reviewed, it could be a file on disk or the contents of a hypothetical app.py or app.R. I tried an alternative approach that's more consistent, but I'm worried this has user experience tradeoffs. feat: Add shinylive url command #20 (comment)

  2. We updated the imports to use typing_extensions for Python < 3.11, but this is causing issues in CI (that I've resolved!). feat: Add shinylive url command #20 (comment). In doing this, I realized the type we were adjusting already existed in this project and suffers from the same problem

    # This is the same as the FileContentJson type in TypeScript.
    class FileContentJson(TypedDict):
    name: str
    content: str
    type: Literal["text", "binary"]
    # Even though TypedDict is available in Python 3.8, because it's used with NotRequired,
    # they should both come from the same typing module.
    # https://peps.python.org/pep-0655/#usage-in-python-3-11
    if sys.version_info >= (3, 11):
    from typing import NotRequired, TypedDict
    else:
    from typing_extensions import NotRequired, TypedDict
    class FileContentJson(TypedDict):
    name: str
    content: str
    type: NotRequired[Literal["text", "binary"]]

@gadenbuie
Copy link
Contributor Author

I've figured out the issue with pip install -e . and typing_extensions:

  1. We use version = attr: shinylive.__version__ in setup.cfg
    version = attr: shinylive.__version__
  2. When installing, pip generates metadata for the package and it needs to load shinylive to determine the package version.
  3. Loading the package requires importing type_annotations, which hasn't been installed yet, causing the ModuleNotFoundError.

It took a bit of exploration, but I've come up with what I think is a pretty good solution:

  1. I've moved the version information into a subpackage: shinylive.version.
  2. In setup.cfg we call version = attr: shinylive.version.SHINYLIVE_PACKAGE_VERSION and in other places we import the constants from .version instead of ._version.
  3. This way, pip can find the package version in the version subpackage without having to evaluate the code in the main shinylive package.

@wch
Copy link
Collaborator

wch commented Jan 8, 2024

For the typing_extensions issue, I believe you should be able to do what we do in shiny, and list it in install_requires:
https://github.com/posit-dev/py-shiny/blob/592cf34397606eed98c1488799e495754ebf8a1f/setup.cfg#L34-L35

I had forgotten that that typing_extensions isn't part of the Python standard library, and so it needs to be listed as an explicit dependency.

@gadenbuie
Copy link
Contributor Author

gadenbuie commented Jan 8, 2024

For the typing_extensions issue, I believe you should be able to do what we do in shiny, and list it in install_requires: posit-dev/py-shiny@592cf34/setup.cfg#L34-L35

I had forgotten that that typing_extensions isn't part of the Python standard library, and so it needs to be listed as an explicit dependency.

@wch Did you see my last comment above? (I know there's a lot going on in this PR its easy to miss.)

In short, it's not about install_requires but rather how shinylive uses a dynamic version that requires pip to run the module code. In other words, I found the source of the problem we paired on together last week and I have a pretty good fix for it.

@wch
Copy link
Collaborator

wch commented Jan 8, 2024

I've figured out the issue with pip install -e . and typing_extensions

Wait, do you mean the lzstring issue? (I don't recall typing_extensions being a problem when we paired on it last week but I could be missing something.)

@gadenbuie
Copy link
Contributor Author

gadenbuie commented Jan 8, 2024

Yeah, we hadn't added typing_extensions yet, but once we did we resurfaced the same problem we had with lzstring. Fundamentally they're caused by the same thing (see above for description). With lzstring, we could move the import statement into a function and delay its evaluation; with typing_extensions we can't use that strategy so I had to figure out the root cause.

@wch
Copy link
Collaborator

wch commented Jan 9, 2024

Hm, it's strange that the same thing happened with typing_extensions -- for shiny and a number of other packages, we use version = attr: shiny.__version__ , and that works fine. I think the fact that it's not working here is a symptom of something else that's weird. I'll take a deeper look into it.

@gadenbuie
Copy link
Contributor Author

gadenbuie commented Jan 9, 2024

@wch What's different about py-shiny is that __version__ is set to a literal value:

__version__ = "0.6.1.9000"

Here in py-shinylive it's set a to an expression, which then forces pip to evaluate some of the packaged code to determine the value. The problem also goes away if we were to do the same here and set __version__ directly rather than storing the version numbers in a separate variable.

@wch
Copy link
Collaborator

wch commented Jan 9, 2024

Ah, OK, I see. That sounds good then. Can you you rename version to _version?

@gadenbuie gadenbuie merged commit 9c5050f into main Jan 12, 2024
5 checks passed
@gadenbuie gadenbuie deleted the feat/shinylive-url branch January 12, 2024 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants