11#!/usr/bin/env python
22# -*- coding: utf-8 -*-
33import logging
4- from collections import Iterable
5- from collections import MutableMapping
4+ from collections .abc import Iterable , MutableMapping
65from collections import defaultdict
7- from decimal import Decimal
86from hashlib import sha1 , sha256
97
108from deepdiff .helper import (strings , numbers , unprocessed , not_hashed , add_to_frozen_set ,
119 convert_item_or_items_into_set_else_none , get_doc ,
1210 convert_item_or_items_into_compiled_regexes_else_none ,
13- get_id , type_is_subclass_of_type_group , type_in_type_group )
11+ get_id , type_is_subclass_of_type_group , type_in_type_group ,
12+ number_to_string , KEY_TO_VAL_STR )
1413from deepdiff .base import Base
1514logger = logging .getLogger (__name__ )
1615
1716try :
1817 import mmh3
19- except ImportError :
20- mmh3 = False
18+ except ImportError : # pragma: no cover
19+ mmh3 = False # pragma: no cover
2120
2221UNPROCESSED = 'unprocessed'
2322MURMUR_SEED = 1203
2726
2827INDEX_VS_ATTRIBUTE = ('[%s]' , '.%s' )
2928
30- KEY_TO_VAL_STR = "{}:{}"
31-
32- ZERO_DECIMAL_CHARACTERS = set ("-0." )
33-
3429
3530def prepare_string_for_hashing (obj , ignore_string_type_changes = False , ignore_string_case = False ):
3631 """
@@ -62,20 +57,23 @@ def __init__(self,
6257 hasher = None ,
6358 ignore_repetition = True ,
6459 significant_digits = None ,
60+ number_format_notation = "f" ,
6561 apply_hash = True ,
6662 ignore_type_in_groups = None ,
6763 ignore_string_type_changes = False ,
6864 ignore_numeric_type_changes = False ,
6965 ignore_type_subclasses = False ,
7066 ignore_string_case = False ,
67+ number_to_string_func = None ,
7168 ** kwargs ):
7269 if kwargs :
7370 raise ValueError (
7471 ("The following parameter(s) are not valid: %s\n "
75- "The valid parameters are obj, hashes, exclude_types,"
76- "exclude_paths, exclude_regex_paths, hasher, ignore_repetition,"
77- "significant_digits, apply_hash, ignore_type_in_groups, ignore_string_type_changes,"
78- "ignore_numeric_type_changes, ignore_type_subclasses, ignore_string_case" ) % ', ' .join (kwargs .keys ()))
72+ "The valid parameters are obj, hashes, exclude_types, significant_digits, "
73+ "exclude_paths, exclude_regex_paths, hasher, ignore_repetition, "
74+ "number_format_notation, apply_hash, ignore_type_in_groups, ignore_string_type_changes, "
75+ "ignore_numeric_type_changes, ignore_type_subclasses, ignore_string_case "
76+ "number_to_string_func" ) % ', ' .join (kwargs .keys ()))
7977 self .obj = obj
8078 exclude_types = set () if exclude_types is None else set (exclude_types )
8179 self .exclude_types_tuple = tuple (exclude_types ) # we need tuple for checking isinstance
@@ -89,6 +87,7 @@ def __init__(self,
8987 self [UNPROCESSED ] = []
9088
9189 self .significant_digits = self .get_significant_digits (significant_digits , ignore_numeric_type_changes )
90+ self .number_format_notation = number_format_notation
9291 self .ignore_type_in_groups = self .get_ignore_types_in_groups (
9392 ignore_type_in_groups = ignore_type_in_groups ,
9493 ignore_string_type_changes = ignore_string_type_changes ,
@@ -102,6 +101,7 @@ def __init__(self,
102101 # testing the individual hash functions for different types of objects.
103102 self .apply_hash = apply_hash
104103 self .type_check_func = type_is_subclass_of_type_group if ignore_type_subclasses else type_in_type_group
104+ self .number_to_string = number_to_string_func or number_to_string
105105
106106 self ._hash (obj , parent = "root" , parents_ids = frozenset ({get_id (obj )}))
107107
@@ -143,7 +143,6 @@ def murmur3_128bit(obj):
143143 return mmh3 .hash128 (obj , MURMUR_SEED )
144144
145145 def __getitem__ (self , obj ):
146- # changed_to_id = False
147146 key = obj
148147 result = None
149148
@@ -230,9 +229,6 @@ def _prep_dict(self, obj, parent, parents_ids=EMPTY_FROZENSET, print_as_attribut
230229 type_str = 'dict'
231230 return "%s:{%s}" % (type_str , result )
232231
233- def _prep_set (self , obj , parent , parents_ids = EMPTY_FROZENSET ):
234- return "set:{}" .format (self ._prep_iterable (obj = obj , parent = parent , parents_ids = parents_ids ))
235-
236232 def _prep_iterable (self , obj , parent , parents_ids = EMPTY_FROZENSET ):
237233
238234 result = defaultdict (int )
@@ -264,17 +260,11 @@ def _prep_iterable(self, obj, parent, parents_ids=EMPTY_FROZENSET):
264260 return result
265261
266262 def _prep_number (self , obj ):
267- if self .significant_digits is not None and (
268- self .ignore_numeric_type_changes or isinstance (obj , (float , complex , Decimal ))):
269- obj_s = ("{:.%sf}" % self .significant_digits ).format (obj )
270-
271- # Special case for 0: "-0.00" should compare equal to "0.00"
272- if set (obj_s ) <= ZERO_DECIMAL_CHARACTERS :
273- obj_s = "0.00"
274- result = "number:{}" .format (obj_s )
275- else :
276- result = KEY_TO_VAL_STR .format (type (obj ).__name__ , obj )
277- return result
263+ type_ = "number" if self .ignore_numeric_type_changes else obj .__class__ .__name__
264+ if self .significant_digits is not None :
265+ obj = self .number_to_string (obj , significant_digits = self .significant_digits ,
266+ number_format_notation = self .number_format_notation )
267+ return KEY_TO_VAL_STR .format (type_ , obj )
278268
279269 def _prep_tuple (self , obj , parent , parents_ids ):
280270 # Checking to see if it has _fields. Which probably means it is a named
@@ -321,9 +311,6 @@ def _hash(self, obj, parent, parents_ids=EMPTY_FROZENSET):
321311 elif isinstance (obj , tuple ):
322312 result = self ._prep_tuple (obj = obj , parent = parent , parents_ids = parents_ids )
323313
324- elif isinstance (obj , (set , frozenset )):
325- result = self ._prep_set (obj = obj , parent = parent , parents_ids = parents_ids )
326-
327314 elif isinstance (obj , Iterable ):
328315 result = self ._prep_iterable (obj = obj , parent = parent , parents_ids = parents_ids )
329316
0 commit comments