Fix __hash__ crashing on unhashable dict attributes - #2995
Open
Sachith77 wants to merge 2 commits into
Open
Conversation
Follow-up to theupdateframework#2973, which fixed Role.__hash__ and DelegatedRole.__hash__. The same bug remains in the other implementations: Signed, Root, MetaFile, Snapshot, Delegations, TargetFile, Targets and Metadata all pass a raw dict to hash(), so hash() raises "TypeError: unhashable type: 'dict'". Timestamp.__hash__ is itself correct but inherits the failure from Signed and MetaFile. All of these classes define __eq__, so __hash__ is required for them to be usable in a set or as a dict key. test_metadata_eq_.py covers __eq__ for exactly these classes but never calls hash(), which is why this went unnoticed. Hash a subset of immutable fields, as theupdateframework#2973 did. unrecognized_fields is excluded throughout since it holds arbitrary nested JSON. Snapshot.meta and Targets.targets contribute len() rather than their keys, to keep hashing O(1) for roles with many entries. Signed-off-by: Sachith Reddy <sachith.24bcs10403@sst.scaler.com>
Sachith77
force-pushed
the
fix/metadata-hash-unhashable-dicts
branch
from
August 24, 2026 20:00
ee0f4ec to
2932e04
Compare
jku
reviewed
Aug 25, 2026
| return hash( | ||
| (self.version, self.length, self.hashes, self.unrecognized_fields) | ||
| ) | ||
| return hash((self.version, self.length)) |
|
|
||
| def __hash__(self) -> int: | ||
| return hash((super().__hash__(), self.meta)) | ||
| return hash((super().__hash__(), len(self.meta))) |
Member
There was a problem hiding this comment.
len(self.meta) sounds a bit strange
| return hash( | ||
| (self.length, self.hashes, self.path, self.unrecognized_fields) | ||
| ) | ||
| return hash((self.length, self.path)) |
|
|
||
| def __hash__(self) -> int: | ||
| return hash((super().__hash__(), self.targets, self.delegations)) | ||
| return hash((super().__hash__(), len(self.targets), self.delegations)) |
Signed-off-by: Sachith Reddy <sachith.24bcs10403@sst.scaler.com>
Contributor
Author
|
Yeah, both are fair. hashes is just dict[str, str] so there was no real reason to drop it, I was being over-careful. And with len(), two snapshots with the same number of entries ended up with the same hash. Used tuple(sorted(...)) in all four since a dict isn't hashable on its own, and the sort keeps it stable if the key order varies. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Follow-up to #2973, which fixed
__hash__forRoleandDelegatedRole.Same problem was still there in the other metadata classes:
Signed,Root,MetaFile,Snapshot,Delegations,TargetFile,Targets, andMetadata. Their__hash__implementations passed raw dictionaries intohash(), which throwsTypeError: unhashable type: 'dict'.Timestamp.__hash__looked fine on its own, but still breaks because it inherits fromSignedandMetaFile.All these classes define
__eq__, so they need a working__hash__too if they're going to be used in sets or as dict keys.test_metadata_eq_.pycovers equality for these classes but never tested hashing, which is probably why this slipped through.Fix follows the same approach as #2973 — hash a subset of immutable fields instead of the raw dicts.
unrecognized_fieldsis left out since it can hold arbitrary nested JSON. Everything else I just converted to tuples.Testing
test_metadata_hashchecks that for each affected class,hash()doesn't raise, equal objects produce equal hashes, and instances can be used as set members. This fails ondevelopwith 8 subtest errors before the fix.test_metadata_hash_ignores_unrecognized_fieldschecks the deliberate exclusion ofunrecognized_fields— objects that only differ in that field are unequal but can share a hash, which is fine given the data model.