Geyser Shiny Python Modules
with Old Faithful

Brian S. Yandell

2 April 2025

Plan of Study

Build a Shiny Module

Shiny FaithFul App Example

Python vs R Module App

inst/build_module/5_python/app_hist.py
from shiny import App, ui
from geyser.hist import *
app_ui = ui.page_fluid(
    hist_input("hist"),
    hist_output("hist"),
    hist_ui("hist"))
def app_server(input, output, session):
    hist_server("hist")
App(app_ui, app_server).run()
inst/build_module/4_moduleServer/appHist.R
library(geyser)
appUI <- bslib::page(
  histInput("hist"), 
  histOutput("hist"),
  histUI("hist"))
appServer <- function(input, output, session) {
  histServer("hist")}
shiny::shinyApp(appUI, appServer)
  • id "hist" connects app_ui and app_server components
  • R camelCase vs Python under_score conventions
  • R bootstrapPage vs Python ui.page_fluid
  • R shinyApp() vs Python App().run()
  • R library & function vs Python import & def

Self-contained App Function

geyser/hist.py
from shiny import App, ui
from geyser.hist import *
import geyser.io as io
def hist_app():
  app_ui = ui.page_fluid(
    hist_input("hist"),
    hist_output("hist"),
    hist_ui("hist"))
  def app_server(input, output, session):
    hist_server("hist")

  app = App(app_ui, app_server)
  io.app_run(app) # replaces app.run()
R/histApp.R
library(geyser)
histApp <- function() {
  ui <- bslib::page(
    histInput("hist"), 
    histOutput("hist"),
    histUI("hist"))
  server <- function(input, output, session) {
    histServer("hist")}
  shiny::shinyApp(ui, server)
}

Shiny kludge:

  • app.run() may fail due to busy port
  • replace with io.app_run(app) to find free port

Python app.run() kludge

R/io.py
def app_run(app, host = "127.0.0.1", port = None):
    import socket
    import webbrowser
    import nest_asyncio

    # Fix runtime event issue.
    nest_asyncio.apply()

    # Find free port.    
    if port is None:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.bind(('', 0))
            port = s.getsockname()[1]

    # Open URL and run app.
    url = f"{host}:{port}"
    webbrowser.open(url)
    app.run(host=host, port=port)

Web interfaces are tricky, and can lead to collisions from multiple runs.

  • nest_asyncio solves RuntimeError: This event loop is already running
  • socket solves busy port problem
  • webbrowser controls URL for display
  • app.run() runs the app

Module Components

geyser/hist.py
from shiny import module, render, ui
@module.server
def hist_server(input, output, session):
  @render.plot
  def main_plot():
    ...
  @render.ui
  def output_bw_adjust():
    ...
@module.ui
def hist_input():
  return ui.card(...)
@module.ui
def hist_output():
  return ui.output_plot("main_plot")
@module.ui
def hist_ui():
  return ui.output_ui("output_bw_adjust")
R/histApp.R
histServer <- function(id) {
  moduleServer(id, function(input, output, session) {
    ns <- session$ns
    output$main_plot <- renderPlot(...)
    output$bw_adjust <- renderUI(...)}}
histInput <- function(id) {
  ns <- NS(id)
  tagList(selectInput(ns("n_breaks"), ...),
          checkboxInput(ns("individual_obs"), ...),
          checkboxInput(ns("density"), ...))}
histOutput <- function(id) {
  ns <- NS(id)
  plotOutput(ns("main_plot"), ...)}
histUI <- function(id) {
  ns <- NS(id)
  uiOutput(ns("bw_adjust"))}

App Server Detail

geyser/hist.py
@module.server
def hist_server(input, output, session):
    @render.plot
    def main_plot():
        fig, ax = plt.subplots()
        hist_data = np.histogram(duration, bins=int(input.n_breaks()), ...)
        if input.individual_obs():
            ax.plot(duration, np.zeros_like(duration), 'r|', markersize=10)
        if input.density():
            kde = gaussian_kde(duration, bw_method=input.bw_adjust())
            ax.plot(x_grid, kde(x_grid), color='blue')
    @render.ui
    def output_bw_adjust():
        if input.density():
            return ui.input_slider("bw_adjust", ...)
R/histApp.R
histServer <- function(id) {
  moduleServer(id, function(input, output, session) {
    ns <- session$ns
    output$main_plot <- renderPlot({
      hist(duration, breaks = as.numeric(input$n_breaks), ...)
      if (input$individual_obs)
        rug(duration)
      if (input$density)
        lines(density(duration, adjust = input$bw_adjust), col = "blue")})
    output$bw_adjust <- renderUI({
      if(input$density) 
        sliderInput(ns("bw_adjust"), ...)})
}
  • inputs individual_obs, n_breaks, density, bw_adjust
  • outputs main_plot, bw_adjust

Module Namespace Handling

geyser/hist.py
from shiny import module, render, ui
@module.server
def hist_server(input, output, session):
  @render.plot
  def main_plot():
  @render.ui
  def output_bw_adjust():
@module.ui
def hist_input():
@module.ui
def hist_output():
@module.ui
def hist_ui():
R/histApp.R
histServer <- function(id) {
  moduleServer(id, function(input, output, session) {
    ns <- session$ns
    sliderInput(ns("bw_adjust"), ...)})}
histInput <- function(id) {
  ns <- NS(id)
  tagList(selectInput(ns("n_breaks"), ...),
          checkboxInput(ns("individual_obs"), ...),
          checkboxInput(ns("density"), ...))}
histOutput <- function(id) {
  ns <- NS(id)
  plotOutput(ns("main_plot"), ...)}
histUI <- function(id) {
  ns <- NS(id)
  uiOutput(ns("bw_adjust"))}
  • @module.server and @module.ui set up python module namespace
  • id, NS(id) and session$ns set up R module namespace
  • ns("xxx") uses shiny namespace in R

Connecting Modules

Connecting Modules across Pages

inst/connect_modules/app_pages.py
from shiny import App, ui
from geyser.hist import *
from geyser.gghist import *
from geyser.ggpoint import *
app_ui = ui.page_navbar(
  ui.nav_panel("hist",
    hist_input("hist"), hist_output("hist"), hist_ui("hist")),
  ui.nav_panel("gghist",
    gghist_input("gghist"), gghist_output("gghist"), gghist_ui("gghist")),
  ui.nav_panel("ggpoint",
    ggpoint_input("ggpoint"), ggpoint_output("ggpoint"), ggpoint_ui("ggpoint")))
def app_server(input, output, session):
  hist_server("hist")
  gghist_server("gghist")
  ggpoint_server("ggpoint")
App(app_ui, app_server).run()
inst/connect_modules/appPages.R
library(geyser)
appUI <- bslib::page_navbar(
  bslib::nav_panel("hist",
    histInput("hist"), histOutput("hist"), histUI("hist")),
  bslib::nav_panel("gghist",
    gghistInput("gghist"), gghistOutput("gghist"), gghistUI("gghist")),
  bslib::nav_panel("ggpoint",
    ggpointInput("ggpoint"), ggpointOutput("ggpoint"), ggpointUI("ggpoint")))
appServer <- function(input, output, session) {
  histServer("hist")
  gghistServer("gghist")
  ggpointServer("ggpoint")}
shiny::shinyApp(appUI, appServer)
  • UIs organize hist, gghist, ggpoint UIs across nav pages
  • Servers connect 3 servers

Connecting with Rows and Columns

Connecting with Rows and Columns

geyser/rows.py
from shiny import App
from geyser.rows import *
app_ui = ui.page_fluid(
  rows_input("rows"),
  rows_ui("rows"),
  title = "Geyser Python Rows Modules")
def app_server(input, output, session):
  rows_server("rows")
App(app_ui, app_server).run()
R/rowsApp.R
library(geyser)
appUI <- bslib::page(
  title = "Geyser R Rows Modules",
  rowsInput("rows"),
  rowsUI("rows")
)
appServer <- function(input, output, session) {
  rowsServer("rows")
}
shiny::shinyApp(appUI, appServer)
  • rows shiny modules organize details across modules
  • Input functions set up datasets (top row)
  • UI functions have hist, gghist, ggpoint columns
  • Server functions connect all 4 modules

Rows & Columns Layout

geyser/rows.py
from shiny import ui, module
from geyser.datasets import *
from geyser.hist import *
from geyser.gghist import *
from geyser.ggpoint import *
@module.ui
def rows_input():
  return ui.row(
    ui.column(6, datasets_input("datasets")),
    ui.column(6, datasets_ui("datasets")))
@module.ui
def rows_ui():
  return ui.row(
    ui.column(4, ui.card(ui.panel_title("hist"),
      hist_input("hist"), hist_output("hist"), hist_ui("hist"))),
    ui.column(4, ui.card(ui.panel_title("gghist"),
      gghist_input("gghist"), gghist_output("gghist"), gghist_ui("gghist"))),
    ui.column(4, ui.card(ui.panel_title("ggpoint"),
      ggpoint_input("ggpoint"), ggpoint_output("ggpoint"), ggpoint_ui("ggpoint"))))
R/rowsApp.R
library(geyser)
rowsInput <- function(id) {
  ns <- NS(id)
  bslib::layout_columns(
    datasetsInput(ns("datasets")),
    datasetsUI(ns("datasets")))
rowsUI <- function(id) {
  ns <- NS(id)
  bslib::layout_columns(
    bslib::card(bslib::card_header("hist"),
      histInput(ns("hist")), histOutput(ns("hist")), histUI(ns("hist"))),
    bslib::card(bslib::card_header("gghist"),
      gghistInput(ns("gghist")), gghistOutput(ns("gghist")), gghistUI(ns("gghist"))),
    bslib::card(bslib::card_header("ggpoint"),
      ggpointInput(ns("ggpoint")), ggpointOutput(ns("ggpoint")), ggpointUI(ns("ggpoint"))))}

Rows Servers Connect Modules

geyser/rows.py
from geyser.datasets import *
from geyser.hist import *
from geyser.gghist import *
from geyser.ggpoint import *
@module.server
def rows_server(input, output, session):
    dataset = datasets_server("datasets")
    hist_server("hist", dataset)
    gghist_server("gghist", dataset)
    ggpoint_server("ggpoint", dataset)
R/rowsApp.R
library(shiny)
library(geyser)
rowsServer <- function(id) {
  moduleServer(id, function(input, output, session) {
    dataset <- datasetsServer("datasets")
    histServer("hist", dataset)
    gghistServer("gghist", dataset)
    ggpointServer("ggpoint", dataset)
  })
}
  • rows module calls 4 other modules
  • datasets module determines dataset and columns
  • dataset data frame is input to other 3 modules

Challenges with Python

  • Need to install geyser package from GitHub or local
    • pip install pip@git+https://github.com/byandell/geyser
    • pip install ~/Documents/GitHub/geyser
  • From bash, the shiny run app_hist.py may not work
  • Running shiny python from within Rstudio with reticulate can fail due to multithreading.
  • Partial workaround with io.r_object()
  • See more notes on README.md

Challenges with Quarto

Quarto with R & Python

Quarto with R & Python Details

GitHub Repo Organization

byandell/geyser
- inst
  - build_module
    - 1_oldFaithful
    - 2_newFaithful
    - 3_callModule
    - 4_moduleServer*
    - 5_python*
  - connect_modules*
  - slideDeck
    - images
    - slidePython.qmd*
- R*: R package code
- geyser*: Python package code

Questions?

byandell.github.io