Fix Var hashing to prevent silent data loss and enable hashability - #7015
Fix Var hashing to prevent silent data loss and enable hashability#7015masenf wants to merge 3 commits into
Conversation
Var.__eq__ builds a BooleanVar rather than returning a bool, and bool-ifying
a Var raises, so a container can never compare Vars. Anything holding a Var
therefore breaks when hashed, and each caller had grown its own workaround.
Introduce Var._hash_key(): a canonical identity tuple containing no Var
objects. __hash__ and equals() both derive from it, so they can no longer
drift apart, and VarData reduces its deps to the same key.
This fixes three bugs:
- Var.__format__ registers the var in _global_vars under hash(self), but the
Literal* subclasses hashed only their value, omitting VarData. Two literals
with the same value and different metadata collided, so interpolating both
into one f-string silently dropped one var's hooks and imports from the
compiled output.
- Var.equals() and VarData.__eq__ raised VarTypeError whenever both sides
carried VarData.deps, because comparing the deps tuples walked into
Var.__eq__.
- ToOperation hashed its _original while comparing unequal to it, so a var
and its .to() view were a guaranteed colliding pair.
NumberVar gets an explicit __hash__, mirroring DateTimeVar; defining __eq__
had left it and BooleanVar unhashable. The per-subclass __hash__ overrides
are removed in favour of _hash_key, and VarData.merge can now dedupe deps,
which was previously impossible.
VarData caches its identity key and hash, since Var.__format__ hashes on
every interpolation, and ToOperation overrides _hash_key to skip the
__getattr__ round trip for its deleted _js_expr. Net effect on the hot
compile path: hash(state_var) 0.90us -> 0.44us, f"{state_var}" 2.27us -> 1.57us.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UbCysGoqoSYh53PNBGpHZQ
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UbCysGoqoSYh53PNBGpHZQ
Greptile SummaryThis PR aligns Var hashing and structural equality around a metadata-aware identity key so interpolation retains required hooks, imports, and dependencies.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| packages/reflex-base/src/reflex_base/vars/base.py | Centralizes Var equality and hashing around a metadata-aware key, safely compares dependency Vars through their structural keys, and caches immutable VarData identity. |
| packages/reflex-base/src/reflex_base/vars/number.py | Preserves the inherited Var hash for NumberVar while removing redundant literal-specific hash implementations. |
| packages/reflex-base/src/reflex_base/vars/color.py | Removes the literal color hash override so hashing follows the canonical Var identity. |
| packages/reflex-base/src/reflex_base/vars/object.py | Removes the literal object hash override in favor of metadata-aware base hashing. |
| packages/reflex-base/src/reflex_base/vars/sequence.py | Removes literal sequence and string hash overrides so all affected types use the unified identity contract. |
| tests/units/test_var.py | Adds regression tests for dependency-safe equality, metadata-sensitive hashes, dependency deduplication, conversion identity, and numeric and boolean hashability. |
Reviews (2): Last reviewed commit: "test: pin numeric literal hashability as..." | Re-trigger Greptile
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
1 issue found and verified against the latest diff
Prompt for AI agents (unresolved issues)
Check if these issues are valid β if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/reflex-base/src/reflex_base/vars/number.py">
<violation number="1" location="packages/reflex-base/src/reflex_base/vars/number.py:64">
P2: When two distinct numeric vars have the same Python hash, a set or dict invokes `NumberVar.__eq__` and then raises `VarTypeError` while boolifying its `BooleanVar` result. Do not expose these overloaded-equality objects as native hash keys without a Python-boolean equality path; use a dedicated structural key instead.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
NumberVar defines __eq__, which drops the inherited __hash__ unless it is restored explicitly. Restoring it is load-bearing in a non-obvious way: without it LiteralNumberVar is unhashable, and since Var.__format__ hashes the var to register it in _global_vars, interpolating any numeric literal into a string raises TypeError far from the cause. Cover it with a test so removing the line fails CI rather than production, and say so in the comment above it. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UbCysGoqoSYh53PNBGpHZQ
|
CI is red on Failing check: Why it isn't this PR's:
Fix ported: none, because there's nothing to port β this isn't a failure with a known fix elsewhere, and it isn't reproducible from the diff. I have not skipped, disabled, or quarantined the test. I'm re-running the failed job once to confirm. If it fails again on an unchanged commit I'll treat it as real and dig in rather than re-running further. Generated by Claude Code |
|
Correction to my previous comment: I said I was re-running the failed job. I attempted it and the API refused β Since I couldn't re-run, I root-caused the test instead. The race is real and specific β for _ in range(3):
link = driver.find_element(By.ID, "link_page_x")
link.click()
assert urlsplit(driver.current_url).path.removesuffix("/") == "/static/x"Every earlier click in this test is wrapped in I'm not fixing it in this PR, for two reasons:
The test is not skipped, disabled, or quarantined. I'll keep watching the PR and will re-check this job's state; if it goes green on an unchanged commit that confirms the flake, and if it fails again I'll treat it as real. A maintainer with re-run permission can settle it immediately. Generated by Claude Code |
Type of change
Description
This PR fixes critical bugs in Var hashing and equality that were causing hooks and imports to be silently dropped from compiled output, and making certain Var types unhashable.
Problems fixed:
Silent data loss on var interpolation: When two vars with identical values but different metadata (hooks, imports, dependencies) were interpolated into the same f-string, they would hash to the same value and collide in
_global_vars, causing one var's metadata to be silently dropped from the compiled output.VarTypeError in Var.equals(): The
Var.equals()method was comparingVarData.depselement-wise, which invokedVar.__eq__(). SinceVar.__eq__()returns aBooleanVarrather than a bool, this raisedVarTypeErrorwhen containers tried to compare Vars.NumberVar and BooleanVar unhashable: Defining
__eq__without explicitly preserving__hash__made these types unhashable, breaking their use in sets and dicts.Solution:
Var._hash_key()method that returns a hashable tuple containing the var's canonical identity (expression, type, and var data). This key contains no Var objects, making it safe for containers to use.Var.__hash__()andVar.equals()to use_hash_key()instead of direct field comparison, ensuring they never invokeVar.__eq__().VarData._identity_keyto convert deps to their hash keys, preventing Var objects from leaking into the identity tuple.VarData.merge()using hash keys to avoid redundant dependencies.VarData._identity_keyand_cached_hashas properties to avoid recomputation during every var interpolation.__hash__overrides from subclasses (NumberVar, BooleanVar, LiteralVar, etc.) that were inconsistent with the base implementation.__hash__on NumberVar to prevent it from becoming unhashable when__eq__is defined.Testing
Var.equals()with vars carrying dependenciesVarData.merge()deduplication.to()conversions and originalsequals()across all var typesChangelog
Added news fragment:
news/+var-hash-identity.bugfix.mdhttps://claude.ai/code/session_01UbCysGoqoSYh53PNBGpHZQ