Skip to content

use strict equality for text, numeric, and binary types #790

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 16, 2022
Merged
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
6 changes: 1 addition & 5 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
# Description

A summary of the changes.

# Checklist:
## Checklist

Please update this checklist as you complete each item:

Expand Down
1 change: 1 addition & 0 deletions docs/source/about/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Unreleased
**Fixed**

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

**Changed**

Expand Down
39 changes: 36 additions & 3 deletions src/idom/core/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def dispatch(
next_value = new(self.value)
else:
next_value = new
if next_value is not self.value:
if not strictly_equal(next_value, self.value):
self.value = next_value
hook.schedule_render()

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

def should_render(self, new: ContextProvider[_StateType]) -> bool:
if self._value is not new._value:
if not strictly_equal(self._value, new._value):
for hook in self._subscribers:
hook.set_context_provider(new)
hook.schedule_render()
Expand Down Expand Up @@ -465,7 +465,10 @@ def use_memo(
elif (
len(memo.deps) != len(dependencies)
# if deps are same length check identity for each item
or any(current is not new for current, new in zip(memo.deps, dependencies))
or not all(
strictly_equal(current, new)
for current, new in zip(memo.deps, dependencies)
)
):
memo.deps = dependencies
changed = True
Expand Down Expand Up @@ -765,3 +768,33 @@ def _schedule_render(self) -> None:
logger.exception(
f"Failed to schedule render via {self._schedule_render_callback}"
)


def strictly_equal(x: Any, y: Any) -> bool:
"""Check if two values are identical or, for a limited set or types, equal.

Only the following types are checked for equality rather than identity:

- ``int``
- ``float``
- ``complex``
- ``str``
- ``bytes``
- ``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,
}
106 changes: 105 additions & 1 deletion tests/test_core/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
import idom
from idom import html
from idom.config import IDOM_DEBUG_MODE
from idom.core.hooks import COMPONENT_DID_RENDER_EFFECT, LifeCycleHook, current_hook
from idom.core.hooks import (
COMPONENT_DID_RENDER_EFFECT,
LifeCycleHook,
current_hook,
strictly_equal,
)
from idom.core.layout import Layout
from idom.core.serve import render_json_patch
from idom.testing import DisplayFixture, HookCatcher, assert_idom_did_log, poll
Expand Down Expand Up @@ -1272,3 +1277,102 @@ def SecondCondition():
set_state.current(False)
await layout.render()
assert used_context_values == ["the-value-1", "the-value-2"]


@pytest.mark.parametrize(
"x, y, result",
[
("text", "text", True),
("text", "not-text", False),
(b"text", b"text", True),
(b"text", b"not-text", False),
(bytearray([1, 2, 3]), bytearray([1, 2, 3]), True),
(bytearray([1, 2, 3]), bytearray([1, 2, 3, 4]), False),
(1.0, 1.0, True),
(1.0, 2.0, False),
(1j, 1j, True),
(1j, 2j, False),
# ints less than 5 and greater than 256 are always identical
(-100000, -100000, True),
(100000, 100000, True),
(123, 456, False),
],
)
def test_strictly_equal(x, y, result):
assert strictly_equal(x, y) is result


STRICT_EQUALITY_VALUE_CONSTRUCTORS = [
lambda: "string-text",
lambda: b"byte-text",
lambda: bytearray([1, 2, 3]),
lambda: bytearray([1, 2, 3]),
lambda: 1.0,
lambda: 10000000,
lambda: 1j,
]


@pytest.mark.parametrize("get_value", STRICT_EQUALITY_VALUE_CONSTRUCTORS)
async def test_use_state_compares_with_strict_equality(get_value):
render_count = idom.Ref(0)
set_state = idom.Ref()

@idom.component
def SomeComponent():
_, set_state.current = idom.use_state(get_value())
render_count.current += 1

async with idom.Layout(SomeComponent()) as layout:
await layout.render()
assert render_count.current == 1
set_state.current(get_value())
with pytest.raises(asyncio.TimeoutError):
await asyncio.wait_for(layout.render(), timeout=0.1)


@pytest.mark.parametrize("get_value", STRICT_EQUALITY_VALUE_CONSTRUCTORS)
async def test_use_effect_compares_with_strict_equality(get_value):
effect_count = idom.Ref(0)
value = idom.Ref("string")
hook = HookCatcher()

@idom.component
@hook.capture
def SomeComponent():
@idom.use_effect(dependencies=[value.current])
def incr_effect_count():
effect_count.current += 1

async with idom.Layout(SomeComponent()) as layout:
await layout.render()
assert effect_count.current == 1
value.current = "string" # new string instance but same value
hook.latest.schedule_render()
await layout.render()
# effect does not trigger
assert effect_count.current == 1


@pytest.mark.parametrize("get_value", STRICT_EQUALITY_VALUE_CONSTRUCTORS)
async def test_use_context_compares_with_strict_equality(get_value):
hook = HookCatcher()
context = idom.create_context(None)
inner_render_count = idom.Ref(0)

@idom.component
@hook.capture
def OuterComponent():
return context(InnerComponent(), value=get_value())

@idom.component
def InnerComponent():
idom.use_context(context)
inner_render_count.current += 1

async with idom.Layout(OuterComponent()) as layout:
await layout.render()
assert inner_render_count.current == 1
hook.latest.schedule_render()
await layout.render()
assert inner_render_count.current == 1