Workflow best practices

Dive Into PyShiny by Appsilon

By Piotr Pasza Storożenko, with Pavel Demin support

Goals

  • Learn some Shiny best practices
  • Get a sense of how large complicated apps are structured
  • Know when you need to refactor your application

Avoid premature refactoring

  • Start by just getting your app to work
  • Take small steps to avoid repetition
  • Ultimately you’re the one who needs to work with the code

When should you refactor?

  • You’re trying to hold too much in your head
  • Changing the app is difficult
  • Other people don’t understand your code
  • You don’t understand your code!

Three main techniques

  • Use functions to generate UI
  • Separate reactive and non-reactive code
  • Modules

We’re only going to cover two

  • Use functions to generate UI
  • Separate reactive and non-reactive code
  • Modules

UI Functions

Background on functions

  • In Python, functions are the best way to improve code quality
    • Don’t Repeat Yourself (DRY) principle
    • Can define variables within the function scope
    • Can write tests against them
    • Easy to use in list comprehensions
  • If your code is getting crazy, start refactoring into functions

Data Science blind spot



Data scientists forget funcitons when writing Shiny UIs

UI elements are just values

  • Recall that Shiny UI elements are just values
    • They can be saved as variables
    • They can be stored in lists
    • They can be passed into functions
    • They can be returned by functions

Using functions

from shiny import App, render, ui

app_ui = ui.page_fluid(
    ui.input_slider("n1", "N", 0, 100, 20),
    ui.input_slider("n2", "N", 0, 100, 20),
    ui.input_slider("n3", "N", 0, 100, 20),
    ui.input_slider("n4", "N", 0, 100, 20),
    ui.input_slider("n5", "N", 0, 100, 20),
    ui.input_slider("n6", "N", 0, 100, 20),
)

app = App(app_ui, None)

Using functions

from shiny import App, render, ui

def my_slider(id, label):
    return ui.input_slider(id, "N", 0, 100, 20)

app_ui = ui.page_fluid(
    my_slider("n1"),
    my_slider("n2"),
    my_slider("n3"),
    my_slider("n4"),
    my_slider("n5"),
)

app = App(app_ui, None)

Applying a function over a list

from shiny import App, render, ui

def my_slider(id):
    return ui.input_slider(id, "N", 0, 100, 20)

ids = ["n1", "n2", "n3", "n4", "n5"]

app_ui = ui.page_fluid(
    [my_slider(x) for x in ids]
)

app = App(app_ui, None)

Iterating across two lists

from shiny import App, render, ui

def my_slider(id, label):
    return ui.input_slider(id, label + "Number", 0, 100, 20)

numbers = ["n1", "n2", "n3", "n4", "n5", "n6"]
labels = ["First", "Second", "Third", "Fourth", "Fifth", "Sixth"]

app_ui = ui.page_fluid(
    [my_slider(x, y) for x, y in zip(numbers, labels)]
)

app = App(app_ui, None)

Separate Reactive and Non-reactive logic

  • Most Shiny app code is non-reactive
    • Drawing plots
    • Summarizing data
    • Interacting with databases
    • (Really, everything except the actual reading of reactive inputs and calcs)
  • It’s fine to include this code inside reactive functions
  • As your app grows you should separate them

Reactivity makes everything harder

  • Reactive context makes them inherently more difficult
    • Harder to debug
    • Harder to test
    • Harder to document
    • Harder to reason about

Pull logic out of the reactive context

  • Non-reactive functions are familliar and predictable
  • You can call them in a notebook
  • You can write unit tests
  • Makes your reactive code much clearer
  • You can reuse them in other contexts

Modules

  • Not going to teach them, but you should know about them
  • Used when you want to encapsulate both UI and server logic together
  • Allows you to package and re-use parts of your application
  • Great for working with teams of developers
  • Essential for large applications

Modules sneak peek

@module.ui
def images_row_ui():
    return ui.layout_columns(
        ui.column(6, ui.output_image("reference_image")),
        ui.column(4, ui.output_image("segmented_image")),
    )

@module.server
def images_row_server(
    input, output, session, file: SegmentationFile, model: ONNXModel
):
    @render.image
    def reference_image():
        return {"src": SegmentationFile.reference_path}

    @render.image
    def segmented_image():
        prediction = file.get_prediction(model)
        img_pred = Image.fromarray(prediction.mask)
        path_tmp = NamedTemporaryFile(suffix=".png", delete=False)
        img_pred.save(path_tmp.name)
        return {"src": path_tmp.name}

Modules sneak peek

@module.ui
def images_row_ui():
    return ui.layout_columns(
        ui.column(6, ui.output_image("reference_image")),
        ui.column(4, ui.output_image("segmented_image")),
    )

@module.server
def images_row_server(
    input, output, session, file: SegmentationFile, model: ONNXModel
):
    @render.image
    def reference_image():
        return {"src": SegmentationFile.reference_path}

    @render.image
    def segmented_image():
        prediction = file.get_prediction(model)
        img_pred = Image.fromarray(prediction.mask)
        path_tmp = NamedTemporaryFile(suffix=".png", delete=False)
        img_pred.save(path_tmp.name)
        return {"src": path_tmp.name}

Modules sneak peek

@module.ui
def images_row_ui():
    return ui.layout_columns(
        ui.column(6, ui.output_image("reference_image")),
        ui.column(4, ui.output_image("segmented_image")),
    )

@module.server
def images_row_server(
    input, output, session, file: SegmentationFile, model: ONNXModel
):
    @render.image
    def reference_image():
        return {"src": SegmentationFile.reference_path}

    @render.image
    def segmented_image():
        prediction = file.get_prediction(model)
        img_pred = Image.fromarray(prediction.mask)
        path_tmp = NamedTemporaryFile(suffix=".png", delete=False)
        img_pred.save(path_tmp.name)
        return {"src": path_tmp.name}

Modules sneak peek

@module.ui
def images_row_ui():
    return ui.layout_columns(
        ui.column(6, ui.output_image("reference_image")),
        ui.column(4, ui.output_image("segmented_image")),
    )

@module.server
def images_row_server(
    input, output, session, file: SegmentationFile, model: ONNXModel
):
    @render.image
    def reference_image():
        return {"src": SegmentationFile.reference_path}

    @render.image
    def segmented_image():
        prediction = file.get_prediction(model)
        img_pred = Image.fromarray(prediction.mask)
        path_tmp = NamedTemporaryFile(suffix=".png", delete=False)
        img_pred.save(path_tmp.name)
        return {"src": path_tmp.name}

Modules sneak peek

@render.ui
def images_rows_ui():
    files = req(dataframe_selected_images())
    html_tags = []
    for i, seg_file in enumerate(files):
        # Creating shiny server module works by calling the server function
        images_row_server(f"image_row_{i}", seg_file, selected_onnx_model())
        html_tags.append(images_row_ui(f"image_row_{i}"))
    return ui.TagList(html_tags)