Skip to content

Commit 2720911

Browse files
authored
use strict equality for text, numeric, and binary types (#790)
1 parent 7bebc4d commit 2720911

File tree

4 files changed

+143
-9
lines changed

4 files changed

+143
-9
lines changed

.github/pull_request_template.md

+1-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
# Description
2-
3-
A summary of the changes.
4-
5-
# Checklist:
1+
## Checklist
62

73
Please update this checklist as you complete each item:
84

docs/source/about/changelog.rst

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Unreleased
2626
**Fixed**
2727

2828
- :issue:`789` - Conditionally rendered components cannot use contexts
29+
- :issue:`773` - Use strict equality check for text, numeric, and binary types in hooks
2930

3031
**Changed**
3132

src/idom/core/hooks.py

+36-3
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def dispatch(
113113
next_value = new(self.value)
114114
else:
115115
next_value = new
116-
if next_value is not self.value:
116+
if not strictly_equal(next_value, self.value):
117117
self.value = next_value
118118
hook.schedule_render()
119119

@@ -317,7 +317,7 @@ def render(self) -> VdomDict:
317317
return vdom("", *self.children)
318318

319319
def should_render(self, new: ContextProvider[_StateType]) -> bool:
320-
if self._value is not new._value:
320+
if not strictly_equal(self._value, new._value):
321321
for hook in self._subscribers:
322322
hook.set_context_provider(new)
323323
hook.schedule_render()
@@ -465,7 +465,10 @@ def use_memo(
465465
elif (
466466
len(memo.deps) != len(dependencies)
467467
# if deps are same length check identity for each item
468-
or any(current is not new for current, new in zip(memo.deps, dependencies))
468+
or not all(
469+
strictly_equal(current, new)
470+
for current, new in zip(memo.deps, dependencies)
471+
)
469472
):
470473
memo.deps = dependencies
471474
changed = True
@@ -765,3 +768,33 @@ def _schedule_render(self) -> None:
765768
logger.exception(
766769
f"Failed to schedule render via {self._schedule_render_callback}"
767770
)
771+
772+
773+
def strictly_equal(x: Any, y: Any) -> bool:
774+
"""Check if two values are identical or, for a limited set or types, equal.
775+
776+
Only the following types are checked for equality rather than identity:
777+
778+
- ``int``
779+
- ``float``
780+
- ``complex``
781+
- ``str``
782+
- ``bytes``
783+
- ``bytearray``
784+
- ``memoryview``
785+
"""
786+
return x is y or (type(x) in _NUMERIC_TEXT_BINARY_TYPES and x == y)
787+
788+
789+
_NUMERIC_TEXT_BINARY_TYPES = {
790+
# numeric
791+
int,
792+
float,
793+
complex,
794+
# text
795+
str,
796+
# binary types
797+
bytes,
798+
bytearray,
799+
memoryview,
800+
}

tests/test_core/test_hooks.py

+105-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
import idom
66
from idom import html
77
from idom.config import IDOM_DEBUG_MODE
8-
from idom.core.hooks import COMPONENT_DID_RENDER_EFFECT, LifeCycleHook, current_hook
8+
from idom.core.hooks import (
9+
COMPONENT_DID_RENDER_EFFECT,
10+
LifeCycleHook,
11+
current_hook,
12+
strictly_equal,
13+
)
914
from idom.core.layout import Layout
1015
from idom.core.serve import render_json_patch
1116
from idom.testing import DisplayFixture, HookCatcher, assert_idom_did_log, poll
@@ -1272,3 +1277,102 @@ def SecondCondition():
12721277
set_state.current(False)
12731278
await layout.render()
12741279
assert used_context_values == ["the-value-1", "the-value-2"]
1280+
1281+
1282+
@pytest.mark.parametrize(
1283+
"x, y, result",
1284+
[
1285+
("text", "text", True),
1286+
("text", "not-text", False),
1287+
(b"text", b"text", True),
1288+
(b"text", b"not-text", False),
1289+
(bytearray([1, 2, 3]), bytearray([1, 2, 3]), True),
1290+
(bytearray([1, 2, 3]), bytearray([1, 2, 3, 4]), False),
1291+
(1.0, 1.0, True),
1292+
(1.0, 2.0, False),
1293+
(1j, 1j, True),
1294+
(1j, 2j, False),
1295+
# ints less than 5 and greater than 256 are always identical
1296+
(-100000, -100000, True),
1297+
(100000, 100000, True),
1298+
(123, 456, False),
1299+
],
1300+
)
1301+
def test_strictly_equal(x, y, result):
1302+
assert strictly_equal(x, y) is result
1303+
1304+
1305+
STRICT_EQUALITY_VALUE_CONSTRUCTORS = [
1306+
lambda: "string-text",
1307+
lambda: b"byte-text",
1308+
lambda: bytearray([1, 2, 3]),
1309+
lambda: bytearray([1, 2, 3]),
1310+
lambda: 1.0,
1311+
lambda: 10000000,
1312+
lambda: 1j,
1313+
]
1314+
1315+
1316+
@pytest.mark.parametrize("get_value", STRICT_EQUALITY_VALUE_CONSTRUCTORS)
1317+
async def test_use_state_compares_with_strict_equality(get_value):
1318+
render_count = idom.Ref(0)
1319+
set_state = idom.Ref()
1320+
1321+
@idom.component
1322+
def SomeComponent():
1323+
_, set_state.current = idom.use_state(get_value())
1324+
render_count.current += 1
1325+
1326+
async with idom.Layout(SomeComponent()) as layout:
1327+
await layout.render()
1328+
assert render_count.current == 1
1329+
set_state.current(get_value())
1330+
with pytest.raises(asyncio.TimeoutError):
1331+
await asyncio.wait_for(layout.render(), timeout=0.1)
1332+
1333+
1334+
@pytest.mark.parametrize("get_value", STRICT_EQUALITY_VALUE_CONSTRUCTORS)
1335+
async def test_use_effect_compares_with_strict_equality(get_value):
1336+
effect_count = idom.Ref(0)
1337+
value = idom.Ref("string")
1338+
hook = HookCatcher()
1339+
1340+
@idom.component
1341+
@hook.capture
1342+
def SomeComponent():
1343+
@idom.use_effect(dependencies=[value.current])
1344+
def incr_effect_count():
1345+
effect_count.current += 1
1346+
1347+
async with idom.Layout(SomeComponent()) as layout:
1348+
await layout.render()
1349+
assert effect_count.current == 1
1350+
value.current = "string" # new string instance but same value
1351+
hook.latest.schedule_render()
1352+
await layout.render()
1353+
# effect does not trigger
1354+
assert effect_count.current == 1
1355+
1356+
1357+
@pytest.mark.parametrize("get_value", STRICT_EQUALITY_VALUE_CONSTRUCTORS)
1358+
async def test_use_context_compares_with_strict_equality(get_value):
1359+
hook = HookCatcher()
1360+
context = idom.create_context(None)
1361+
inner_render_count = idom.Ref(0)
1362+
1363+
@idom.component
1364+
@hook.capture
1365+
def OuterComponent():
1366+
return context(InnerComponent(), value=get_value())
1367+
1368+
@idom.component
1369+
def InnerComponent():
1370+
idom.use_context(context)
1371+
inner_render_count.current += 1
1372+
1373+
async with idom.Layout(OuterComponent()) as layout:
1374+
await layout.render()
1375+
assert inner_render_count.current == 1
1376+
hook.latest.schedule_render()
1377+
await layout.render()
1378+
assert inner_render_count.current == 1

0 commit comments

Comments
 (0)