Skip to content
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to Shiny for Python will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [UNRELEASED]

### New features

* A `default` parameter is now available to reactive reads (e.g., `input.val(default=None)`). When provided, and the read results in a `MISSING` value, the `default` value is returned instead of raising a `SilentException` (#2100)

### Improvements

* A warning now occurs when a reactive read (e.g., `input.val()`) results in a `SilentException`. This most commonly occurs when attempting to read an input that doesn't exist in the UI. Since this behavior is sometimes desirable (mainly for dynamic UI reasons), the warning can be suppressed via `input.val(warn_if_missing=False)`. (#2100)

## [1.5.0] - 2025-09-11

### New features
Expand Down
119 changes: 107 additions & 12 deletions shiny/reactive/_reactives.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ class Value(Generic[T]):
value
An optional initial value.
read_only
If ``True``, then the reactive value cannot be `set()`.
If ``True``, then the reactive value cannot be `set()`. For internal purposes,
a string containing an input ID can also be provided.

Returns
-------
Expand Down Expand Up @@ -107,29 +108,84 @@ class Value(Generic[T]):
# - Value(1) works, with T is inferred to be int.
@overload
def __init__(
self, value: MISSING_TYPE = MISSING, *, read_only: bool = False
self, value: MISSING_TYPE = MISSING, *, read_only: bool | str = False
) -> None: ...

@overload
def __init__(self, value: T, *, read_only: bool = False) -> None: ...
def __init__(self, value: T, *, read_only: bool | str = False) -> None: ...

# If `value` is MISSING, then `get()` will raise a SilentException, until a new
# value is set. Calling `unset()` will set the value to MISSING.
def __init__(
self, value: T | MISSING_TYPE = MISSING, *, read_only: bool = False
self,
value: T | MISSING_TYPE = MISSING,
*,
read_only: bool | str = False,
) -> None:
self._value: T | MISSING_TYPE = value
self._read_only: bool = read_only
self._value_dependents: Dependents = Dependents()
self._is_set_dependents: Dependents = Dependents()
if isinstance(read_only, str):
self._read_only = True
self._id = read_only
else:
self._read_only = read_only
self._id = None

def __call__(self) -> T:
return self.get()
@overload
def __call__(
self, *, default: MISSING_TYPE = MISSING, warn_if_missing: bool = True
) -> T: ...

@overload
def __call__(
self, *, default: None = None, warn_if_missing: bool = True
) -> T | None: ...

@overload
def __call__(self, *, default: T, warn_if_missing: bool = True) -> T: ...

def __call__(
self,
*,
default: MISSING_TYPE | T | None = MISSING,
warn_if_missing: bool = True,
) -> T | None:
return self.get(default=default, warn_if_missing=warn_if_missing)

@overload
def get(
self, *, default: MISSING_TYPE = MISSING, warn_if_missing: bool = True
) -> T: ...

def get(self) -> T:
@overload
def get(
self, *, default: None = None, warn_if_missing: bool = True
) -> T | None: ...

@overload
def get(self, *, default: T, warn_if_missing: bool = True) -> T: ...

def get(
self,
*,
default: MISSING_TYPE | T | None = MISSING,
warn_if_missing: bool = True,
) -> T | None:
"""
Read the reactive value.

Parameters
----------
default
A default value to return if the reactive value is not set. If no default is
provided and the value is not set, then a
:class:`~shiny.types.SilentException` is raised.
warn_if_missing
If ``True`` (the default), log an INFO message when a
:class:`~shiny.types.SilentException` is raised because the value is not
set. Set to ``False`` to suppress this logging.

Returns
-------
:
Expand All @@ -138,17 +194,21 @@ def get(self) -> T:
Raises
------
:class:`~shiny.types.SilentException`
If the value is not set.
If the value is not set and no default is provided.
RuntimeError
If called from outside a reactive function.
"""

self._value_dependents.register()

if isinstance(self._value, MISSING_TYPE):
raise SilentException
if not isinstance(self._value, MISSING_TYPE):
return self._value
if not isinstance(default, MISSING_TYPE):
return default

return self._value
if warn_if_missing:
self._throw_missing_value_warning(self._id)
raise SilentException

def set(self, value: T) -> bool:
"""
Expand Down Expand Up @@ -221,6 +281,41 @@ def freeze(self) -> None:
"""
self._value = MISSING

@staticmethod
def _throw_missing_value_warning(id: str | None):
from ..session import get_current_session

# Try to provide some context about where the missing value access occurred.
output_ctx = ""
session = get_current_session()
if session and not session.is_stub_session():
name = session.clientdata._current_output_name
if name:
output_ctx = f"inside of the '{name}' render function"

if id is None:
warnings.warn(
f"Attempted to read a `reactive.value` {output_ctx}, but it is not "
"currently set to a value. As a result, a SilentException occurred. "
"To avoid this warning, provide a default value when creating (or "
"reading) a reactive value, e.g., `val = reactive.value(None)` ",
ReactiveWarning,
stacklevel=4,
)
else:
warnings.warn(
f"Attempted to read `input.{id}()` {output_ctx}, before an input value "
"was actually available. As a result, a SilentException occurred. "
"This may be desirable behavior, but if not, consider the following:\n"
"1. Check that the input ID is correct and matches the ID used in the UI.\n"
"2. To change logic depending on whether or not an input is available, "
f"use `if '{id}' in input:` before reading the value.\n"
f"3. If a default value makes sense, try `input.{id}(default=...)`.\n"
f"To silence this message, set `input.{id}(warn_if_missing=False)`.",
ReactiveWarning,
stacklevel=4,
)


value = Value

Expand Down
14 changes: 10 additions & 4 deletions shiny/render/_data_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,9 @@ def cell_selection(self) -> CellSelection:
"""
browser_cell_selection = cast(
BrowserCellSelection,
self._get_session().input[f"{self.output_id}_cell_selection"](),
self._get_session().input[f"{self.output_id}_cell_selection"](
warn_if_missing=False
),
)

cell_selection = as_cell_selection(
Expand Down Expand Up @@ -527,7 +529,7 @@ def data_view_rows(self) -> tuple[int, ...]:
"""
input_data_view_rows = self._get_session().input[
f"{self.output_id}_data_view_rows"
]()
](warn_if_missing=False)
return tuple(input_data_view_rows)

# @reactive_calc_method
Expand All @@ -540,7 +542,9 @@ def sort(self) -> tuple[ColumnSort, ...]:
:
An array of `col`umn number and _is `desc`ending_ information.
"""
column_sort = self._get_session().input[f"{self.output_id}_column_sort"]()
column_sort = self._get_session().input[f"{self.output_id}_column_sort"](
warn_if_missing=False
)
return tuple(column_sort)

# @reactive_calc_method
Expand All @@ -553,7 +557,9 @@ def filter(self) -> tuple[ColumnFilter, ...]:
:
An array of `col`umn number and `value` information. If the column type is a number, a tuple of `(min, max)` is used for `value`. If no min (or max) value is set, `None` is used in its place. If the column type is a string, the string value is used for `value`.
"""
column_filter = self._get_session().input[f"{self.output_id}_column_filter"]()
column_filter = self._get_session().input[f"{self.output_id}_column_filter"](
warn_if_missing=False
)
return tuple(column_filter)

def _reset_reactives(self) -> None:
Expand Down
2 changes: 1 addition & 1 deletion shiny/session/_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -1388,7 +1388,7 @@ def __getitem__(self, key: str) -> Value[Any]:
# dependencies on input values that haven't been received from client
# yet.
if key not in self._map:
self._map[key] = Value[Any](read_only=True)
self._map[key] = Value[Any](read_only=key)

return self._map[key]

Expand Down
Loading