diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst index e7a0b6ccf..a9ddbe854 100644 --- a/docs/source/about/changelog.rst +++ b/docs/source/about/changelog.rst @@ -20,6 +20,7 @@ Unreleased - :pull:`1251` - Substitute client-side usage of ``react`` with ``preact``. - :pull:`1239` - Script elements no longer support behaving like effects. They now strictly behave like plain HTML script elements. - :pull:`1255` - The ``reactpy.html`` module has been modified to allow for auto-creation of any HTML nodes. For example, you can create a ```` element by calling ``html.data_table()``. +- :pull:`1256` - Change ``set_state`` comparison method to check equality with ``==`` more consistently. **Removed** @@ -34,6 +35,7 @@ Unreleased v1.1.0 ------ +:octicon:`milestone` *released on 2024-11-24* **Fixed** @@ -69,6 +71,7 @@ v1.1.0 v1.0.2 ------ +:octicon:`milestone` *released on 2023-07-03* **Fixed** @@ -77,6 +80,7 @@ v1.0.2 v1.0.1 ------ +:octicon:`milestone` *released on 2023-06-16* **Changed** diff --git a/src/reactpy/core/hooks.py b/src/reactpy/core/hooks.py index 0ece8cccf..5a3c9fd13 100644 --- a/src/reactpy/core/hooks.py +++ b/src/reactpy/core/hooks.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import contextlib from collections.abc import Coroutine, MutableMapping, Sequence from logging import getLogger from types import FunctionType @@ -517,18 +518,30 @@ def strictly_equal(x: Any, y: Any) -> bool: - ``bytearray`` - ``memoryview`` """ - return x is y or (type(x) in _NUMERIC_TEXT_BINARY_TYPES and x == y) - - -_NUMERIC_TEXT_BINARY_TYPES = { - # numeric - int, - float, - complex, - # text - str, - # binary types - bytes, - bytearray, - memoryview, -} + # Return early if the objects are not the same type + if type(x) is not type(y): + return False + + # Compare the source code of lambda and local functions + if ( + hasattr(x, "__qualname__") + and ("" in x.__qualname__ or "" in x.__qualname__) + and hasattr(x, "__code__") + ): + if x.__qualname__ != y.__qualname__: + return False + + return all( + getattr(x.__code__, attr) == getattr(y.__code__, attr) + for attr in dir(x.__code__) + if attr.startswith("co_") + and attr not in {"co_positions", "co_linetable", "co_lines"} + ) + + # Check via the `==` operator if possible + if hasattr(x, "__eq__"): + with contextlib.suppress(Exception): + return x == y # type: ignore + + # Fallback to identity check + return x is y diff --git a/tests/test_core/test_hooks.py b/tests/test_core/test_hooks.py index 550d35cbc..1f444cb68 100644 --- a/tests/test_core/test_hooks.py +++ b/tests/test_core/test_hooks.py @@ -159,7 +159,7 @@ def Counter(): await layout.render() -async def test_set_state_checks_identity_not_equality(display: DisplayFixture): +async def test_set_state_checks_equality_not_identity(display: DisplayFixture): r_1 = reactpy.Ref("value") r_2 = reactpy.Ref("value") @@ -219,12 +219,12 @@ def TestComponent(): await client_r_2_button.click() await poll_event_count.until_equals(2) - await poll_render_count.until_equals(2) + await poll_render_count.until_equals(1) await client_r_2_button.click() await poll_event_count.until_equals(3) - await poll_render_count.until_equals(2) + await poll_render_count.until_equals(1) async def test_simple_input_with_use_state(display: DisplayFixture): @@ -1172,6 +1172,28 @@ def test_strictly_equal(x, y, result): assert strictly_equal(x, y) is result +def test_strictly_equal_named_closures(): + assert strictly_equal(lambda: "text", lambda: "text") is True + assert strictly_equal(lambda: "text", lambda: "not-text") is False + + def x(): + return "text" + + def y(): + return "not-text" + + def generator(): + def z(): + return "text" + + return z + + assert strictly_equal(x, x) is True + assert strictly_equal(x, y) is False + assert strictly_equal(x, generator()) is False + assert strictly_equal(generator(), generator()) is True + + STRICT_EQUALITY_VALUE_CONSTRUCTORS = [ lambda: "string-text", lambda: b"byte-text", diff --git a/tests/test_testing.py b/tests/test_testing.py index 63439c194..a6517abc0 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -173,7 +173,7 @@ def test_list_logged_excptions(): assert logged_errors == [the_error] -async def test_hostwap_update_on_change(display: DisplayFixture): +async def test_hotswap_update_on_change(display: DisplayFixture): """Ensure shared hotswapping works 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): hotswap component to be updated """ - def make_next_count_constructor(count): - """We need to construct a new function so they're different when we set_state""" + def hotswap_1(): + return html.div({"id": "hotswap-1"}, 1) - def constructor(): - count.current += 1 - return html.div({"id": f"hotswap-{count.current}"}, count.current) + def hotswap_2(): + return html.div({"id": "hotswap-2"}, 2) - return constructor + def hotswap_3(): + return html.div({"id": "hotswap-3"}, 3) @component def ButtonSwapsDivs(): count = Ref(0) + mount, hostswap = _hotswap(update_on_change=True) async def on_click(event): - mount(make_next_count_constructor(count)) - - incr = html.button({"on_click": on_click, "id": "incr-button"}, "incr") - - mount, make_hostswap = _hotswap(update_on_change=True) - mount(make_next_count_constructor(count)) - hotswap_view = make_hostswap() - - return html.div(incr, hotswap_view) + count.set_current(count.current + 1) + if count.current == 1: + mount(hotswap_1) + if count.current == 2: + mount(hotswap_2) + if count.current == 3: + mount(hotswap_3) + + return html.div( + html.button({"on_click": on_click, "id": "incr-button"}, "incr"), + hostswap(), + ) await display.show(ButtonSwapsDivs) client_incr_button = await display.page.wait_for_selector("#incr-button") + await client_incr_button.click() await display.page.wait_for_selector("#hotswap-1") await client_incr_button.click() await display.page.wait_for_selector("#hotswap-2")