Fix Var hashing to prevent silent data loss and enable hashability
Type of change
- Bug fix (non-breaking change which fixes an issue)
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:
- Introduced
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. - Updated
Var.__hash__()andVar.equals()to use_hash_key()instead of direct field comparison, ensuring they never invokeVar.__eq__(). - Updated
VarData._identity_keyto convert deps to their hash keys, preventing Var objects from leaking into the identity tuple. - Deduplicated deps in
VarData.merge()using hash keys to avoid redundant dependencies. - Cached
VarData._identity_keyand_cached_hashas properties to avoid recomputation during every var interpolation. - Removed redundant
__hash__overrides from subclasses (NumberVar, BooleanVar, LiteralVar, etc.) that were inconsistent with the base implementation. - Explicitly preserved
__hash__on NumberVar to prevent it from becoming unhashable when__eq__is defined.
Testing
- Added comprehensive unit tests covering:
Var.equals()with vars carrying dependenciesVarData.merge()deduplication- Hash collision prevention for literal vars with different metadata
- Hash consistency between
.to()conversions and originals - Hash consistency with
equals()across all var types - Verification that hash keys contain no Var objects
- Hashability of NumberVar and BooleanVar
- All new tests pass; existing tests continue to pass.
Changelog
Added news fragment: news/+var-hash-identity.bugfix.md