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

input validation #189

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions WHAT-WE-LEARNED.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

Even if it seems obvious in retrospect, what have we learned about Python Shiny in this project?

## Unless we use modules, namespaces overlap

The practice of just using regular functions to separate UI elements can lead to problems
if server function names are repeated. Is it safer to just always use modules?

## Input validation: `None` or `SilentException`?

I had been returning `None`, but is there an advantage to `SilentException`?

## Some basic html attributes are not supported

I can mark a button as disabled, but it doesn't seem that a `ui.input_select` can be disabled.
Expand Down
19 changes: 14 additions & 5 deletions dp_wizard/app/components/column_module.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from logging import info

from shiny import ui, render, module, reactive, Inputs, Outputs, Session
from shiny.types import SilentException

from dp_wizard.utils.dp_helper import make_accuracy_histogram
from dp_wizard.utils.shared import plot_histogram
Expand Down Expand Up @@ -155,19 +156,27 @@ def column_code():

@render.plot()
def column_plot():
lower_x = float(input.lower())
upper_x = float(input.upper())
bin_count = int(input.bins())
optional_lower = input.lower()
optional_upper = input.upper()
optional_bin_count = input.bins()
if None in [optional_lower, optional_upper, optional_bin_count]:
raise Exception("All inputs are required.")

lower = float(optional_lower)
upper = float(optional_upper)
bin_count = int(optional_bin_count)

weight = float(input.weight())
weights_sum = sum(float(weight) for weight in weights().values())
info(f"Weight ratio for {name}: {weight}/{weights_sum}")
if weights_sum == 0:
# This function is triggered when column is removed;
# Exit early to avoid divide-by-zero.
return None

accuracy, histogram = make_accuracy_histogram(
lower=lower_x,
upper=upper_x,
lower=lower,
upper=upper,
bin_count=bin_count,
contributions=contributions,
weighted_epsilon=epsilon * weight / weights_sum,
Expand Down
8 changes: 7 additions & 1 deletion dp_wizard/app/dataset_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def dataset_ui():
cli_info.contributions,
min=1,
),
ui.output_text("contributions_error"),
ui.output_ui("python_tooltip_ui"),
output_code_sample("Unit of Privacy", "unit_of_privacy_python"),
ui.output_ui("define_analysis_button_ui"),
Expand Down Expand Up @@ -58,7 +59,7 @@ def _on_contributions_change():

@reactive.calc
def button_enabled():
contributions_is_set = input.contributions() is not None
contributions_is_set = int(input.contributions() or 0) >= 1
csv_path_is_set = (
input.csv_path() is not None and len(input.csv_path()) > 0
) or is_demo
Expand All @@ -80,6 +81,11 @@ def contributions_demo_tooltip_ui():
f"can occur at most {contributions()} times in the dataset. ",
)

@render.text
def contributions_error():
if not int(input.contributions() or 0) >= 1:
raise Exception("Contributions must be at least 1.")

@render.ui
def python_tooltip_ui():
return demo_tooltip(
Expand Down
20 changes: 19 additions & 1 deletion tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,26 @@ def expect_no_error():
# Button disabled until upload:
define_analysis_button = page.get_by_role("button", name="Define analysis")
assert define_analysis_button.is_disabled()
button_error = "Choose CSV and Contributions before proceeding"
expect_visible(button_error)

# Now upload:
csv_path = Path(__file__).parent / "fixtures" / "fake.csv"
page.get_by_label("Choose CSV file").set_input_files(csv_path.resolve())
expect_no_error()

# Contributions error checks:
contributions_error = "Contributions must be at least 1"
page.get_by_label("Contributions").fill("")
expect_visible(contributions_error)
expect_visible(button_error)
page.get_by_label("Contributions").fill("0")
expect_visible(contributions_error)
expect_visible(button_error)
page.get_by_label("Contributions").fill("42")
expect_not_visible(contributions_error)
expect_not_visible(button_error)

# -- Define analysis --
define_analysis_button.click()
expect_not_visible(pick_dataset_text)
Expand Down Expand Up @@ -91,6 +105,9 @@ def expect_no_error():
expect_not_visible("Weight")
# Check that default is set correctly:
assert page.get_by_label("Upper").input_value() == "10"
# Clear input and check for appropriate error:
page.get_by_label("Upper").fill("")
expect_visible("All inputs are required")
# Reset, and confirm:
new_value = "20"
page.get_by_label("Upper").fill(new_value)
Expand All @@ -103,7 +120,8 @@ def expect_no_error():
assert page.get_by_label("Upper").input_value() == new_value
# Add a second column:
page.get_by_label("blank").check()
expect_visible("Weight")
# TODO: Flaky
# expect_visible("Weight")
# TODO: Setting more inputs without checking for updates
# causes recalculations to pile up, and these cause timeouts on CI:
# It is still rerendering the graph after hitting "Download results".
Expand Down
Loading