1- import unicodedata
21from hashlib import sha256
3-
4- from django .core .cache import cache
2+ import inspect
53
64from cache_helper import settings
75from cache_helper .exceptions import CacheKeyCreationError
6+ from cache_helper .interfaces import CacheHelperCacheable
7+
8+
9+ def get_function_cache_key (func_type , func_name , func_args , func_kwargs ):
10+ if func_type in ['method' , 'function' ]:
11+ args_string = _sanitize_args (* func_args , ** func_kwargs )
12+ elif func_type == 'class_method' :
13+ # In this case, since we are dealing with a class method, the first arg to the function
14+ # will be the class. Since the name of the class and function is already built in to the
15+ # cache key, we can bypass the class variable and instead slice from the first index.
16+ args_string = _sanitize_args (* func_args [1 :], ** func_kwargs )
17+ key = '{func_name}{args_string}' .format (func_name = func_name , args_string = args_string )
18+ return key
819
9- # List of Control Characters not useable by memcached
10- CONTROL_CHARACTERS = set ([chr (i ) for i in range (0 , 33 )])
11- CONTROL_CHARACTERS .add (chr (127 ))
1220
13- def sanitize_key (key , max_length = 250 ):
21+ def get_hashed_cache_key (key ):
1422 """
15- Truncates key to keep it under memcached char limit. Replaces with hash.
16- Remove control characters b/c of memcached restriction on control chars.
23+ Given the intermediate key produced by a function call along with its args + kwargs,
24+ performs a sha256 hash on the utf-8 encoded version of the key, and returns the result
1725 """
18- key = '' .join ([c for c in key if c not in CONTROL_CHARACTERS ])
19- key_length = len (key )
20- # django memcached backend will, by default, add a prefix. Account for this in max
21- # key length. '%s:%s:%s'.format()
22- version_length = len (str (getattr (cache , 'version' , '' )))
23- prefix_length = len (settings .CACHE_MIDDLEWARE_KEY_PREFIX )
24- # +2 for the colons
25- max_length -= (version_length + prefix_length + 2 )
26- if key_length > max_length :
27- the_hash = sha256 (key .encode ('utf-8' )).hexdigest ()
28- # sha256 always 64 chars.
29- key = key [:max_length - 64 ] + the_hash
30- return key
26+ key_hash = sha256 (key .encode ('utf-8' , errors = 'ignore' )).hexdigest ()
27+ return key_hash
3128
3229
33- def _sanitize_args (args = [], kwargs = {} ):
30+ def _sanitize_args (* args , ** kwargs ):
3431 """
3532 Creates unicode key from all kwargs/args
36- -Note: comma separate args in order to prevent poo (1,2), poo (12, None) corner-case collisions...
33+ -Note: comma separate args in order to prevent foo (1,2), foo (12, None) corner-case collisions...
3734 """
38- key = ";{0};{1}"
39- kwargs_key = ""
35+ key = ";{args_key};{kwargs_key}"
4036 args_key = _plumb_collections (args )
4137 kwargs_key = _plumb_collections (kwargs )
42- return key .format (args_key , kwargs_key )
38+ return key .format (args_key = args_key , kwargs_key = kwargs_key )
4339
4440
45- def _func_type (func ):
46- argnames = func . __code__ . co_varnames [: func . __code__ . co_argcount ]
47- if len ( argnames ) > 0 :
48- if argnames [ 0 ] == 'self' :
49- return 'method'
50- elif argnames [ 0 ] == 'cls' :
51- return 'class_method'
52- return 'function '
41+ def get_function_type (func ):
42+ """
43+ Gets the type of the given function
44+ """
45+ if 'self' in inspect . getargspec ( func ). args :
46+ return 'method'
47+ if 'cls' in inspect . getargspec ( func ). args :
48+ return 'class_method '
5349
50+ if inspect .isfunction (func ):
51+ return 'function'
5452
55- def get_normalized_term (term , dash_replacement = '' ):
56- term = str (term )
57- if isinstance (term , bytes ):
58- term = term .decode ('utf-8' )
59- term = unicodedata .normalize ('NFKD' , term ).encode ('ascii' , 'ignore' ).decode ('utf-8' )
60- term = term .lower ()
61- term = term .strip ()
62- return term
53+ return None
6354
6455
65- def _func_info (func , args ):
66- func_type = _func_type (func )
67- lineno = ":%s" % func .__code__ .co_firstlineno
56+ def get_function_name (func ):
57+ func_type = get_function_type (func )
6858
69- if func_type == 'function' :
70- name = "." .join ([func .__module__ , func .__name__ ]) + lineno
59+ if func_type in ['method' , 'class_method' , 'function' ]:
60+ name = '{func_module}.{qualified_name}' \
61+ .format (func_module = func .__module__ , qualified_name = func .__qualname__ )
7162 return name
72- elif func_type == 'class_method' :
73- class_name = args [0 ].__name__
74- else :
75- class_name = args [0 ].__class__ .__name__
76- name = "." .join ([func .__module__ , class_name , func .__name__ ]) + lineno
77- return name
7863
64+ return None
7965
80- def _cache_key (func_name , func_type , args , kwargs ):
81- if func_type in ['method' , 'function' ]:
82- args_string = _sanitize_args (args , kwargs )
83- elif func_type == 'class_method' :
84- args_string = _sanitize_args (args [1 :], kwargs )
85- key = '%s%s' % (func_name , args_string )
86- return key
8766
8867def _plumb_collections (input_item ):
8968 """
@@ -96,18 +75,19 @@ def _plumb_collections(input_item):
9675 if hasattr (input_item , '__iter__' ):
9776 if isinstance (input_item , dict ):
9877 # Py3k Compatibility nonsense...
99- remains = [[(k ,v ) for k , v in input_item .items ()].__iter__ ()]
78+ remains = [[(k , v ) for k , v in input_item .items ()].__iter__ ()]
10079 # because dictionary iterators yield tuples, it would appear
10180 # to be 2 levels per dictionary, but that seems unexpected.
10281 level -= 1
10382 else :
10483 remains = [input_item .__iter__ ()]
10584 else :
106- return get_normalized_term (input_item )
85+ return _get_object_cache_key (input_item )
10786
10887 while len (remains ) > 0 :
10988 if settings .MAX_DEPTH is not None and level > settings .MAX_DEPTH :
110- raise CacheKeyCreationError ('Function args or kwargs have too many nested collections for current MAX_DEPTH' )
89+ raise CacheKeyCreationError (
90+ 'Function args or kwargs have too many nested collections for current MAX_DEPTH' )
11191 current_iterator = remains .pop ()
11292 level += 1
11393 while True :
@@ -127,7 +107,8 @@ def _plumb_collections(input_item):
127107 hashed_list = []
128108
129109 for k , v in current_item .items ():
130- hashed_list .append ((sha256 (str (k ).encode ('utf-8' )).hexdigest (), v ))
110+ item_cache_key = _get_object_cache_key (k )
111+ hashed_list .append ((sha256 (item_cache_key .encode ('utf-8' )).hexdigest (), v ))
131112
132113 hashed_list = sorted (hashed_list , key = lambda t : t [0 ])
133114 remains .append (current_iterator )
@@ -139,7 +120,8 @@ def _plumb_collections(input_item):
139120 hashed_list = []
140121
141122 for item in current_item :
142- hashed_list .append (sha256 (str (item ).encode ('utf-8' )).hexdigest ())
123+ item_cache_key = _get_object_cache_key (item )
124+ hashed_list .append (sha256 (item_cache_key .encode ('utf-8' )).hexdigest ())
143125
144126 hashed_list = sorted (hashed_list )
145127 remains .append (current_iterator )
@@ -150,10 +132,23 @@ def _plumb_collections(input_item):
150132 remains .append (current_item .__iter__ ())
151133 break
152134 else :
153- current_item_string = '{0},' .format (get_normalized_term (current_item ))
135+ current_item_string = '{0},' .format (_get_object_cache_key (current_item ))
154136 return_list .append (current_item_string )
155137 continue
156138 # trim trailing comma
157139 return_string = '' .join (return_list )
158140 # trim last ',' because it lacks significant meaning.
159141 return return_string [:- 1 ]
142+
143+
144+ def _get_object_cache_key (obj ):
145+ """
146+ Function used to get the individual cache key for objects. Checks if the
147+ object is an instance of CacheHelperCacheable, which means it will have a
148+ get_cache_helper_key function defined for it which will be used as the key.
149+ Otherwise, just uses the string representation of the object.
150+ """
151+ if isinstance (obj , CacheHelperCacheable ):
152+ return obj .get_cache_helper_key ()
153+ else :
154+ return str (obj )
0 commit comments