Skip to content

Commit a54ce4e

Browse files
authored
Change set_state comparison method (#1256)
1 parent 754dc11 commit a54ce4e

File tree

4 files changed

+78
-34
lines changed

4 files changed

+78
-34
lines changed

docs/source/about/changelog.rst

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Unreleased
2020
- :pull:`1251` - Substitute client-side usage of ``react`` with ``preact``.
2121
- :pull:`1239` - Script elements no longer support behaving like effects. They now strictly behave like plain HTML script elements.
2222
- :pull:`1255` - The ``reactpy.html`` module has been modified to allow for auto-creation of any HTML nodes. For example, you can create a ``<data-table>`` element by calling ``html.data_table()``.
23+
- :pull:`1256` - Change ``set_state`` comparison method to check equality with ``==`` more consistently.
2324

2425
**Removed**
2526

@@ -34,6 +35,7 @@ Unreleased
3435

3536
v1.1.0
3637
------
38+
:octicon:`milestone` *released on 2024-11-24*
3739

3840
**Fixed**
3941

@@ -69,6 +71,7 @@ v1.1.0
6971

7072
v1.0.2
7173
------
74+
:octicon:`milestone` *released on 2023-07-03*
7275

7376
**Fixed**
7477

@@ -77,6 +80,7 @@ v1.0.2
7780

7881
v1.0.1
7982
------
83+
:octicon:`milestone` *released on 2023-06-16*
8084

8185
**Changed**
8286

src/reactpy/core/hooks.py

+28-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import asyncio
4+
import contextlib
45
from collections.abc import Coroutine, MutableMapping, Sequence
56
from logging import getLogger
67
from types import FunctionType
@@ -517,18 +518,30 @@ def strictly_equal(x: Any, y: Any) -> bool:
517518
- ``bytearray``
518519
- ``memoryview``
519520
"""
520-
return x is y or (type(x) in _NUMERIC_TEXT_BINARY_TYPES and x == y)
521-
522-
523-
_NUMERIC_TEXT_BINARY_TYPES = {
524-
# numeric
525-
int,
526-
float,
527-
complex,
528-
# text
529-
str,
530-
# binary types
531-
bytes,
532-
bytearray,
533-
memoryview,
534-
}
521+
# Return early if the objects are not the same type
522+
if type(x) is not type(y):
523+
return False
524+
525+
# Compare the source code of lambda and local functions
526+
if (
527+
hasattr(x, "__qualname__")
528+
and ("<lambda>" in x.__qualname__ or "<locals>" in x.__qualname__)
529+
and hasattr(x, "__code__")
530+
):
531+
if x.__qualname__ != y.__qualname__:
532+
return False
533+
534+
return all(
535+
getattr(x.__code__, attr) == getattr(y.__code__, attr)
536+
for attr in dir(x.__code__)
537+
if attr.startswith("co_")
538+
and attr not in {"co_positions", "co_linetable", "co_lines"}
539+
)
540+
541+
# Check via the `==` operator if possible
542+
if hasattr(x, "__eq__"):
543+
with contextlib.suppress(Exception):
544+
return x == y # type: ignore
545+
546+
# Fallback to identity check
547+
return x is y

tests/test_core/test_hooks.py

+25-3
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def Counter():
159159
await layout.render()
160160

161161

162-
async def test_set_state_checks_identity_not_equality(display: DisplayFixture):
162+
async def test_set_state_checks_equality_not_identity(display: DisplayFixture):
163163
r_1 = reactpy.Ref("value")
164164
r_2 = reactpy.Ref("value")
165165

@@ -219,12 +219,12 @@ def TestComponent():
219219
await client_r_2_button.click()
220220

221221
await poll_event_count.until_equals(2)
222-
await poll_render_count.until_equals(2)
222+
await poll_render_count.until_equals(1)
223223

224224
await client_r_2_button.click()
225225

226226
await poll_event_count.until_equals(3)
227-
await poll_render_count.until_equals(2)
227+
await poll_render_count.until_equals(1)
228228

229229

230230
async def test_simple_input_with_use_state(display: DisplayFixture):
@@ -1172,6 +1172,28 @@ def test_strictly_equal(x, y, result):
11721172
assert strictly_equal(x, y) is result
11731173

11741174

1175+
def test_strictly_equal_named_closures():
1176+
assert strictly_equal(lambda: "text", lambda: "text") is True
1177+
assert strictly_equal(lambda: "text", lambda: "not-text") is False
1178+
1179+
def x():
1180+
return "text"
1181+
1182+
def y():
1183+
return "not-text"
1184+
1185+
def generator():
1186+
def z():
1187+
return "text"
1188+
1189+
return z
1190+
1191+
assert strictly_equal(x, x) is True
1192+
assert strictly_equal(x, y) is False
1193+
assert strictly_equal(x, generator()) is False
1194+
assert strictly_equal(generator(), generator()) is True
1195+
1196+
11751197
STRICT_EQUALITY_VALUE_CONSTRUCTORS = [
11761198
lambda: "string-text",
11771199
lambda: b"byte-text",

tests/test_testing.py

+21-16
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def test_list_logged_excptions():
173173
assert logged_errors == [the_error]
174174

175175

176-
async def test_hostwap_update_on_change(display: DisplayFixture):
176+
async def test_hotswap_update_on_change(display: DisplayFixture):
177177
"""Ensure shared hotswapping works
178178
179179
This basically means that previously rendered views of a hotswap component get updated
@@ -183,34 +183,39 @@ async def test_hostwap_update_on_change(display: DisplayFixture):
183183
hotswap component to be updated
184184
"""
185185

186-
def make_next_count_constructor(count):
187-
"""We need to construct a new function so they're different when we set_state"""
186+
def hotswap_1():
187+
return html.div({"id": "hotswap-1"}, 1)
188188

189-
def constructor():
190-
count.current += 1
191-
return html.div({"id": f"hotswap-{count.current}"}, count.current)
189+
def hotswap_2():
190+
return html.div({"id": "hotswap-2"}, 2)
192191

193-
return constructor
192+
def hotswap_3():
193+
return html.div({"id": "hotswap-3"}, 3)
194194

195195
@component
196196
def ButtonSwapsDivs():
197197
count = Ref(0)
198+
mount, hostswap = _hotswap(update_on_change=True)
198199

199200
async def on_click(event):
200-
mount(make_next_count_constructor(count))
201-
202-
incr = html.button({"on_click": on_click, "id": "incr-button"}, "incr")
203-
204-
mount, make_hostswap = _hotswap(update_on_change=True)
205-
mount(make_next_count_constructor(count))
206-
hotswap_view = make_hostswap()
207-
208-
return html.div(incr, hotswap_view)
201+
count.set_current(count.current + 1)
202+
if count.current == 1:
203+
mount(hotswap_1)
204+
if count.current == 2:
205+
mount(hotswap_2)
206+
if count.current == 3:
207+
mount(hotswap_3)
208+
209+
return html.div(
210+
html.button({"on_click": on_click, "id": "incr-button"}, "incr"),
211+
hostswap(),
212+
)
209213

210214
await display.show(ButtonSwapsDivs)
211215

212216
client_incr_button = await display.page.wait_for_selector("#incr-button")
213217

218+
await client_incr_button.click()
214219
await display.page.wait_for_selector("#hotswap-1")
215220
await client_incr_button.click()
216221
await display.page.wait_for_selector("#hotswap-2")

0 commit comments

Comments
 (0)