99from pathlib import Path
1010from enum import Enum
1111import re
12- from deepdiff .helper import (strings , numbers , times , unprocessed , not_hashed , add_to_frozen_set ,
12+ from deepdiff .helper import (strings , numbers , only_numbers , times , unprocessed , not_hashed , add_to_frozen_set ,
1313 convert_item_or_items_into_set_else_none , get_doc , ipranges ,
1414 convert_item_or_items_into_compiled_regexes_else_none ,
1515 get_id , type_is_subclass_of_type_group , type_in_type_group ,
@@ -276,10 +276,13 @@ def __init__(self,
276276 sha1hex : Callable [[Union [str , bytes ]], str ] = sha1hex
277277
278278 def __getitem__ (self , obj : Any , extract_index : Optional [int ] = 0 ) -> Any :
279- return self ._getitem (self .hashes , obj , extract_index = extract_index , use_enum_value = self .use_enum_value )
279+ return self ._getitem (self .hashes , obj , extract_index = extract_index ,
280+ use_enum_value = self .use_enum_value ,
281+ ignore_numeric_type_changes = self .ignore_numeric_type_changes )
280282
281283 @staticmethod
282- def _getitem (hashes : Dict [Any , Any ], obj : Any , extract_index : Optional [int ] = 0 , use_enum_value : bool = False ) -> Any :
284+ def _getitem (hashes : Dict [Any , Any ], obj : Any , extract_index : Optional [int ] = 0 ,
285+ use_enum_value : bool = False , ignore_numeric_type_changes : bool = False ) -> Any :
283286 """
284287 extract_index is zero for hash and 1 for count and None to get them both.
285288 To keep it backward compatible, we only get the hash by default so it is set to zero by default.
@@ -292,6 +295,7 @@ def _getitem(hashes: Dict[Any, Any], obj: Any, extract_index: Optional[int] = 0,
292295 key = BoolObj .FALSE
293296 elif use_enum_value and isinstance (obj , Enum ):
294297 key = obj .value
298+ key = DeepHash ._make_hash_key_for_lookup (key , ignore_numeric_type_changes = ignore_numeric_type_changes )
295299
296300 result_n_count : Tuple [Any , int ] = (None , 0 ) # type: ignore
297301
@@ -310,9 +314,10 @@ def _getitem(hashes: Dict[Any, Any], obj: Any, extract_index: Optional[int] = 0,
310314 return result_n_count if extract_index is None else result_n_count [extract_index ]
311315
312316 def __contains__ (self , obj : Any ) -> bool :
317+ key = self ._make_hash_key (obj )
313318 result = False
314319 try :
315- result = obj in self .hashes
320+ result = key in self .hashes
316321 except (TypeError , KeyError ):
317322 result = False
318323 if not result :
@@ -325,21 +330,32 @@ def get(self, key: Any, default: Any = None, extract_index: Optional[int] = 0) -
325330 It can extract the hash for a given key that is already calculated when extract_index=0
326331 or the count of items that went to building the object whenextract_index=1.
327332 """
328- return self .get_key (self .hashes , key , default = default , extract_index = extract_index )
333+ return self .get_key (self .hashes , key , default = default , extract_index = extract_index ,
334+ ignore_numeric_type_changes = self .ignore_numeric_type_changes )
329335
330336 @staticmethod
331- def get_key (hashes : Dict [Any , Any ], key : Any , default : Any = None , extract_index : Optional [int ] = 0 , use_enum_value : bool = False ) -> Any :
337+ def get_key (hashes : Dict [Any , Any ], key : Any , default : Any = None , extract_index : Optional [int ] = 0 ,
338+ use_enum_value : bool = False , ignore_numeric_type_changes : bool = False ) -> Any :
332339 """
333340 get_key method for the hashes dictionary.
334341 It can extract the hash for a given key that is already calculated when extract_index=0
335342 or the count of items that went to building the object whenextract_index=1.
336343 """
337344 try :
338- result = DeepHash ._getitem (hashes , key , extract_index = extract_index , use_enum_value = use_enum_value )
345+ result = DeepHash ._getitem (hashes , key , extract_index = extract_index ,
346+ use_enum_value = use_enum_value ,
347+ ignore_numeric_type_changes = ignore_numeric_type_changes )
339348 except KeyError :
340349 result = default
341350 return result
342351
352+ @staticmethod
353+ def _unwrap_hash_key (key : Any ) -> Any :
354+ """Unwrap a (type, value) hash key back to the original value for public API."""
355+ if isinstance (key , tuple ) and len (key ) == 2 and isinstance (key [0 ], type ) and isinstance (key [1 ], only_numbers ):
356+ return key [1 ]
357+ return key
358+
343359 def _get_objects_to_hashes_dict (self , extract_index : Optional [int ] = 0 ) -> Dict [Any , Any ]:
344360 """
345361 A dictionary containing only the objects to hashes,
@@ -348,6 +364,7 @@ def _get_objects_to_hashes_dict(self, extract_index: Optional[int] = 0) -> Dict[
348364 """
349365 result = dict_ ()
350366 for key , value in self .hashes .items ():
367+ key = self ._unwrap_hash_key (key )
351368 if key is UNPROCESSED_KEY :
352369 result [key ] = value
353370 else :
@@ -377,13 +394,13 @@ def __bool__(self) -> bool:
377394 return bool (self .hashes )
378395
379396 def keys (self ) -> Any :
380- return self .hashes .keys ()
397+ return [ self ._unwrap_hash_key ( k ) for k in self . hashes .keys ()]
381398
382399 def values (self ) -> Generator [Any , None , None ]:
383400 return (i [0 ] for i in self .hashes .values ()) # Just grab the item and not its count
384401
385402 def items (self ) -> Generator [Tuple [Any , Any ], None , None ]:
386- return ((i , v [0 ]) for i , v in self .hashes .items ())
403+ return ((self . _unwrap_hash_key ( i ) , v [0 ]) for i , v in self .hashes .items ())
387404
388405 def _prep_obj (self , obj : Any , parent : str , parents_ids : frozenset = EMPTY_FROZENSET , is_namedtuple : bool = False , is_pydantic_object : bool = False ) -> HashTuple :
389406 """prepping objects"""
@@ -555,6 +572,26 @@ def _prep_tuple(self, obj: tuple, parent: str, parents_ids: frozenset) -> HashTu
555572 result , counts = self ._prep_obj (obj , parent , parents_ids = parents_ids , is_namedtuple = True )
556573 return result , counts
557574
575+ def _make_hash_key (self , obj : Any ) -> Any :
576+ """
577+ Create a key for the hashes dict that distinguishes numeric types.
578+
579+ In Python, 1 == 1.0 and hash(1) == hash(1.0), so int and float values
580+ collide as dict keys. When ignore_numeric_type_changes is False, we wrap
581+ numeric objects as (type, value) tuples so that each type gets its own
582+ cache entry and its own hash.
583+ """
584+ if not self .ignore_numeric_type_changes and isinstance (obj , only_numbers ):
585+ return (type (obj ), obj )
586+ return obj
587+
588+ @staticmethod
589+ def _make_hash_key_for_lookup (obj : Any , ignore_numeric_type_changes : bool = False ) -> Any :
590+ """Static version of _make_hash_key for use in static accessor methods."""
591+ if not ignore_numeric_type_changes and isinstance (obj , only_numbers ):
592+ return (type (obj ), obj )
593+ return obj
594+
558595 def _hash (self , obj : Any , parent : str , parents_ids : frozenset = EMPTY_FROZENSET ) -> HashTuple :
559596 """The main hash method"""
560597 counts = 1
@@ -573,8 +610,9 @@ def _hash(self, obj: Any, parent: str, parents_ids: frozenset = EMPTY_FROZENSET)
573610 obj = obj .value
574611 else :
575612 result = not_hashed
613+ hash_key = self ._make_hash_key (obj )
576614 try :
577- result , counts = self .hashes [obj ]
615+ result , counts = self .hashes [hash_key ]
578616 except (TypeError , KeyError ):
579617 pass
580618 else :
@@ -662,7 +700,7 @@ def gen():
662700 # The hashes will be later used for comparing the objects.
663701 # Object to hash when possible otherwise ObjectID to hash
664702 try :
665- self .hashes [obj ] = (result , counts )
703+ self .hashes [hash_key ] = (result , counts )
666704 except TypeError :
667705 obj_id = get_id (obj )
668706 self .hashes [obj_id ] = (result , counts )
0 commit comments