Skip to content

Commit 883febf

Browse files
committed
Add tests, update changelog
1 parent 15ee83d commit 883febf

File tree

3 files changed

+184
-7
lines changed

3 files changed

+184
-7
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to Shiny for Python will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [UNRELEASED]
9+
10+
### New features
11+
12+
* 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)
13+
14+
### Improvements
15+
16+
* 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)
17+
818
## [1.5.0] - 2025-09-11
919

1020
### New features

shiny/reactive/_reactives.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ class Value(Generic[T]):
7272
value
7373
An optional initial value.
7474
read_only
75-
If ``True``, then the reactive value cannot be `set()`. For internal use,
76-
this value may also be a string (of the input ID).
75+
If ``True``, then the reactive value cannot be `set()`. For internal purposes,
76+
a string containing an input ID can also be provided.
7777
7878
Returns
7979
-------

tests/pytest/test_reactives.py

Lines changed: 172 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,14 @@ async def test_reactive_value_unset():
101101
with isolate():
102102
assert v.is_set() is False
103103
with pytest.raises(SilentException):
104-
v()
104+
v(warn_if_missing=False)
105105

106106
val: int = 0
107107

108108
@effect()
109109
def o():
110110
nonlocal val
111-
val = v()
111+
val = v(warn_if_missing=False)
112112

113113
await flush()
114114
assert o._exec_count == 1
@@ -128,7 +128,7 @@ def o():
128128
with isolate():
129129
assert v.is_set() is False
130130
with pytest.raises(SilentException):
131-
v()
131+
v(warn_if_missing=False)
132132

133133

134134
# ======================================================================
@@ -1021,7 +1021,7 @@ async def test_event_silent_exception():
10211021
x = Value[bool]()
10221022

10231023
@effect()
1024-
@event(x)
1024+
@event(lambda: x(warn_if_missing=False))
10251025
def _():
10261026
nonlocal n_times
10271027
n_times += 1
@@ -1052,7 +1052,7 @@ async def test_event_silent_exception_async():
10521052

10531053
async def req_fn() -> int:
10541054
await asyncio.sleep(0)
1055-
x()
1055+
x(warn_if_missing=False)
10561056
return 1234
10571057

10581058
@effect()
@@ -1365,3 +1365,170 @@ async def obs():
13651365
a.set(4)
13661366
await flush()
13671367
assert obs._exec_count == 2
1368+
1369+
1370+
# ======================================================================
1371+
# reactive.Value.get() with default parameter
1372+
# ======================================================================
1373+
@pytest.mark.asyncio
1374+
async def test_reactive_value_get_with_default():
1375+
# Test with unset value - should return default
1376+
v = Value[int]()
1377+
1378+
with isolate():
1379+
assert v.get(default=42) == 42
1380+
assert v.get(default=0) == 0
1381+
assert v.get(default=None) is None
1382+
1383+
# Test after setting value - should return value, not default
1384+
v.set(100)
1385+
with isolate():
1386+
assert v.get(default=42) == 100
1387+
assert v.get(default=0) == 100
1388+
1389+
# Test after unsetting - should return default again
1390+
v.unset()
1391+
with isolate():
1392+
assert v.get(default=99) == 99
1393+
1394+
1395+
@pytest.mark.asyncio
1396+
async def test_reactive_value_call_with_default():
1397+
# Test that __call__ also supports default parameter
1398+
v = Value[str]()
1399+
1400+
with isolate():
1401+
assert v(default="hello") == "hello"
1402+
assert v(default=None) is None
1403+
1404+
v.set("world")
1405+
with isolate():
1406+
assert v(default="hello") == "world"
1407+
1408+
v.unset()
1409+
with isolate():
1410+
assert v(default="fallback") == "fallback"
1411+
1412+
1413+
@pytest.mark.asyncio
1414+
async def test_reactive_value_default_in_reactive_context():
1415+
# Test using default parameter within reactive contexts
1416+
v = Value[int]()
1417+
result = None
1418+
1419+
@effect()
1420+
def obs():
1421+
nonlocal result
1422+
result = v.get(default=10)
1423+
1424+
await flush()
1425+
assert result == 10
1426+
assert obs._exec_count == 1
1427+
1428+
# Setting the value should invalidate and return new value
1429+
v.set(50)
1430+
await flush()
1431+
assert result == 50
1432+
assert obs._exec_count == 2
1433+
1434+
# Unsetting should use default again
1435+
v.unset()
1436+
await flush()
1437+
assert result == 10
1438+
assert obs._exec_count == 3
1439+
1440+
1441+
@pytest.mark.asyncio
1442+
async def test_reactive_value_no_default_raises_silent_exception():
1443+
# Test that without default, unset value still raises SilentException
1444+
v = Value[int]()
1445+
1446+
with isolate():
1447+
with pytest.raises(SilentException):
1448+
v.get()
1449+
with pytest.raises(SilentException):
1450+
v()
1451+
1452+
1453+
# ======================================================================
1454+
# reactive.Value warning behavior
1455+
# ======================================================================
1456+
@pytest.mark.asyncio
1457+
async def test_reactive_value_missing_warning():
1458+
# Test that reading unset value with warn_if_missing=True logs warning
1459+
v = Value[int]()
1460+
1461+
with isolate():
1462+
with pytest.warns(
1463+
ReactiveWarning, match="Attempted to read a `reactive.value`"
1464+
):
1465+
try:
1466+
v.get()
1467+
except SilentException:
1468+
pass
1469+
1470+
1471+
@pytest.mark.asyncio
1472+
async def test_reactive_value_suppress_warning():
1473+
# Test that warn_if_missing=False suppresses the warning
1474+
v = Value[int]()
1475+
1476+
with isolate():
1477+
# Should not produce a warning
1478+
with pytest.raises(SilentException):
1479+
v.get(warn_if_missing=False)
1480+
1481+
1482+
@pytest.mark.asyncio
1483+
async def test_reactive_value_default_no_warning():
1484+
# Test that providing a default doesn't generate warning
1485+
v = Value[int]()
1486+
1487+
with isolate():
1488+
# Should not raise or warn when default is provided
1489+
result = v.get(default=42)
1490+
assert result == 42
1491+
1492+
1493+
@pytest.mark.asyncio
1494+
async def test_input_value_missing_warning():
1495+
# Test that reading unset input value shows input-specific warning
1496+
conn = MockConnection()
1497+
session = App(ui.TagList(), None)._create_session(conn)
1498+
input = session.input
1499+
1500+
# Access an input that doesn't exist yet
1501+
with isolate():
1502+
with pytest.warns(
1503+
ReactiveWarning, match=r"Attempted to read `input\.test_input\(\)`"
1504+
):
1505+
try:
1506+
input.test_input()
1507+
except SilentException:
1508+
pass
1509+
1510+
1511+
@pytest.mark.asyncio
1512+
async def test_input_value_with_default():
1513+
# Test that input values can use default parameter
1514+
conn = MockConnection()
1515+
session = App(ui.TagList(), None)._create_session(conn)
1516+
input = session.input
1517+
1518+
with isolate():
1519+
# Should return default without warning or exception
1520+
result = input.test_input(default="fallback")
1521+
assert result == "fallback"
1522+
1523+
1524+
@pytest.mark.asyncio
1525+
async def test_input_value_suppress_warning():
1526+
# Test that warn_if_missing=False suppresses input warning
1527+
conn = MockConnection()
1528+
session = App(ui.TagList(), None)._create_session(conn)
1529+
input = session.input
1530+
1531+
with isolate():
1532+
# Should not produce a warning
1533+
with pytest.raises(SilentException):
1534+
input.test_input(warn_if_missing=False)

0 commit comments

Comments
 (0)