Skip to content
Open
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
5 changes: 5 additions & 0 deletions packages/reflex-base/news/7015.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix hooks and imports being silently dropped from the compiled output when two
vars with the same value but different metadata were interpolated into the same
f-string. Var hashing is now derived from the same identity `Var.equals` uses,
which also fixes `Var.equals` raising `VarTypeError` for vars that carry
dependencies, and makes `NumberVar` and `BooleanVar` hashable.
93 changes: 56 additions & 37 deletions packages/reflex-base/src/reflex_base/vars/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,13 @@ def merge(*all: VarData | None) -> VarData | None:
*(var_data.imports for var_data in all_var_datas)
)

deps = [dep for var_data in all_var_datas for dep in var_data.deps]
deps = list(
{
dep._hash_key(): dep
for var_data in all_var_datas
for dep in var_data.deps
}.values()
)

positions = list(
dict.fromkeys(
Expand Down Expand Up @@ -435,16 +441,20 @@ def __bool__(self) -> bool:
or self.app_wraps
)

@functools.cached_property
def _identity_key(self) -> tuple:
"""Return a hashable key for ``__eq__`` and ``__hash__``.
"""A hashable key for ``__eq__`` and ``__hash__``.

``components`` and ``app_wraps`` hold ``BaseComponent`` instances whose
``__eq__`` override drops the default hash. Use component identity for
embedded components because they can contribute hooks/imports, and use
the compiler's app-wrap registry key for wrappers so fresh provider
instances with the same role still compare equal. App wraps are a set
of required roles, so a ``frozenset`` keeps identity insensitive to the
order vars happened to merge in (``a + b`` and ``b + a`` stay equal).
``deps`` holds Vars, which a container can never compare because
``Var.__eq__`` builds a ``BooleanVar``, so they are reduced to their
``_hash_key()``. ``components`` and ``app_wraps`` hold ``BaseComponent``
instances whose ``__eq__`` override drops the default hash. Use
component identity for embedded components because they can contribute
hooks/imports, and use the compiler's app-wrap registry key for
wrappers so fresh provider instances with the same role still compare
equal. App wraps are a set of required roles, so a ``frozenset`` keeps
identity insensitive to the order vars happened to merge in
(``a + b`` and ``b + a`` stay equal).

Returns:
A hashable tuple uniquely identifying this VarData.
Expand All @@ -454,7 +464,7 @@ def _identity_key(self) -> tuple:
self.field_name,
self.imports,
self.hooks,
self.deps,
tuple(dep._hash_key() for dep in self.deps),
self.position,
tuple(id(component) for component in self.components),
frozenset(
Expand All @@ -474,15 +484,24 @@ def __eq__(self, other: object) -> bool:
"""
if not isinstance(other, VarData):
return NotImplemented
return self._identity_key() == other._identity_key()
return self._identity_key == other._identity_key

def __hash__(self) -> int:
"""Hash consistent with ``__eq__``.

Returns:
A hash over render-time fields and hashable component metadata.
"""
return hash(self._identity_key())
return self._cached_hash

@functools.cached_property
def _cached_hash(self) -> int:
"""The hash, cached because every Var interpolation recomputes it.

Returns:
A hash over render-time fields and hashable component metadata.
"""
return hash(self._identity_key)

@classmethod
def from_state(cls, state: type[BaseState] | str, field_name: str = "") -> VarData:
Expand Down Expand Up @@ -704,13 +723,28 @@ def __post_init__(self):
_var_data=VarData.merge(self._var_data, var_data_),
)

def _hash_key(self) -> tuple[Any, ...]:
"""Return the canonical identity of this var.

``__eq__`` builds a ``BooleanVar`` rather than returning a bool, and
bool-ifying a Var raises, so a Var can never be compared by a container.
Every structural comparison goes through this key instead, which holds
no Var objects and is therefore safe to nest in tuples, dicts and sets.
Subclasses that need a different identity override this rather than
``__hash__``, so hashing and ``equals`` can never drift apart.

Returns:
A hashable tuple uniquely identifying this var.
"""
return (self._js_expr, self._var_type, self._get_all_var_data())

def __hash__(self) -> int:
"""Define a hash function for the var.

Returns:
The hash of the var.
"""
return hash((self._js_expr, self._var_type, self._var_data))
return hash(self._hash_key())

def _get_all_var_data(self) -> VarData | None:
"""Get all VarData associated with the Var.
Expand Down Expand Up @@ -740,11 +774,7 @@ def equals(self, other: Var) -> builtins.bool:
Returns:
Whether the vars are equal.
"""
return (
self._js_expr == other._js_expr
and self._var_type == other._var_type
and self._get_all_var_data() == other._get_all_var_data()
)
return self._hash_key() == other._hash_key()

@overload
def _replace(
Expand Down Expand Up @@ -1553,13 +1583,17 @@ def __post_init__(self):
"""Post initialization."""
object.__delattr__(self, "_js_expr")

def __hash__(self) -> int:
"""Calculate the hash value of the object.
def _hash_key(self) -> tuple[Any, ...]:
"""Return the canonical identity of this var.

Identical to ``Var._hash_key``, but reads the expression straight off
``_original``: ``__post_init__`` deletes ``_js_expr``, so the inherited
version would pay a ``__getattr__`` round trip on every hash.

Returns:
int: The hash value of the object.
A hashable tuple uniquely identifying this var.
"""
return hash(self._original)
return (self._original._js_expr, self._var_type, self._get_all_var_data())

def _get_all_var_data(self) -> VarData | None:
"""Get all the var data.
Expand Down Expand Up @@ -2144,21 +2178,6 @@ def _cached_get_all_var_data(self: VarProtocol) -> VarData | None:
self._var_data,
)

def __hash__(self: DataclassInstance) -> int:
"""Calculate the hash of the object.

Returns:
The hash of the object.
"""
return hash((
type(self).__name__,
*[
getattr(self, field.name)
for field in dataclasses.fields(self)
if field.name not in ["_js_expr", "_var_data", "_var_type"]
],
))


_PY_AND_IMPORT: ImportDict = {
f"$/{constants.Dirs.STATE_PATH}": [ImportVar(tag="pyAnd")],
Expand Down
13 changes: 0 additions & 13 deletions packages/reflex-base/src/reflex_base/vars/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,6 @@ def create(
_var_value=value,
)

def __hash__(self) -> int:
"""Get the hash of the var.

Returns:
The hash of the var.
"""
return hash((
self.__class__.__name__,
self._var_value.color,
self._var_value.alpha,
self._var_value.shade,
))

@cached_property_no_lock
def _cached_var_name(self) -> str:
"""The name of the var.
Expand Down
22 changes: 6 additions & 16 deletions packages/reflex-base/src/reflex_base/vars/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ def raise_unsupported_operand_types(
class NumberVar(Var[NUMBER_T], python_types=(int, float, decimal.Decimal)):
"""Base class for immutable number vars."""

# Load-bearing: defining __eq__ below drops the inherited hash, which would
# leave every numeric var unhashable, including LiteralNumberVar. Var.__format__
# hashes, so removing this breaks interpolating any numeric literal into a
# string. test_numeric_literal_var_hashability_is_load_bearing pins it.
__hash__ = Var.__hash__
Comment thread
masenf marked this conversation as resolved.

def __add__(self, other: number_types) -> NumberVar:
"""Add two numbers.

Expand Down Expand Up @@ -964,14 +970,6 @@ def json(self) -> str:
raise PrimitiveUnserializableToJSONError(msg)
return json.dumps(self._var_value)

def __hash__(self) -> int:
"""Calculate the hash value of the object.

Returns:
int: The hash value of the object.
"""
return hash((type(self).__name__, self._var_value))

@classmethod
def _get_all_var_data_without_creating_var(
cls, value: float | int | decimal.Decimal
Expand Down Expand Up @@ -1032,14 +1030,6 @@ def json(self) -> str:
"""
return "true" if self._var_value else "false"

def __hash__(self) -> int:
"""Calculate the hash value of the object.

Returns:
int: The hash value of the object.
"""
return hash((type(self).__name__, self._var_value))

@classmethod
def _get_all_var_data_without_creating_var(cls, value: bool) -> VarData | None:
"""Get all the var data without creating the var.
Expand Down
8 changes: 0 additions & 8 deletions packages/reflex-base/src/reflex_base/vars/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,14 +456,6 @@ def json(self) -> str:
keys_and_values.append(f"{key.json()}:{value.json()}")
return "{" + ", ".join(keys_and_values) + "}"

def __hash__(self) -> int:
"""Get the hash of the var.

Returns:
The hash of the var.
"""
return hash((type(self).__name__, self._js_expr))

@classmethod
def _get_all_var_data_without_creating_var(
cls,
Expand Down
29 changes: 0 additions & 29 deletions packages/reflex-base/src/reflex_base/vars/sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,14 +659,6 @@ def _cached_get_all_var_data(self) -> VarData | None:
self._var_data,
)

def __hash__(self) -> int:
"""Get the hash of the var.

Returns:
The hash of the var.
"""
return hash((self.__class__.__name__, self._js_expr))

def json(self) -> str:
"""Get the JSON representation of the var.

Expand Down Expand Up @@ -1501,14 +1493,6 @@ def create(
_var_value=value,
)

def __hash__(self) -> int:
"""Get the hash of the var.

Returns:
The hash of the var.
"""
return hash((type(self).__name__, self._var_value))

def json(self) -> str:
"""Get the JSON representation of the var.

Expand Down Expand Up @@ -2117,19 +2101,6 @@ def create(
_var_value=value,
)

def __hash__(self) -> int:
"""Get the hash of the var.

Returns:
The hash of the var.
"""
return hash((
self.__class__.__name__,
self._var_value.start,
self._var_value.stop,
self._var_value.step,
))

@cached_property_no_lock
def _cached_var_name(self) -> str:
"""The name of the var.
Expand Down
Loading
Loading