/caliendo/facade.py

https://github.com/buzzfeed/caliendo · Python · 450 lines · 402 code · 16 blank · 32 comment · 14 complexity · 89e599071911265425b6969c7a6eaff8 MD5 · raw file

  1. from hashlib import sha1
  2. import os
  3. import sys
  4. import inspect
  5. import importlib
  6. import types
  7. from contextlib import contextmanager
  8. import caliendo
  9. from caliendo import util
  10. from caliendo import config
  11. from caliendo import call_descriptor
  12. from caliendo import counter
  13. from caliendo import prompt
  14. from caliendo.__init__ import UNDEFINED
  15. from caliendo.hooks import Hook
  16. USE_CALIENDO = config.should_use_caliendo()
  17. if USE_CALIENDO:
  18. from caliendo.db.flatfiles import delete_io
  19. def should_exclude(type_or_instance, exclusion_list):
  20. """
  21. Tests whether an object should be simply returned when being wrapped
  22. """
  23. if type_or_instance in exclusion_list: # Check class definition
  24. return True
  25. if type(type_or_instance) in exclusion_list: # Check instance type
  26. return True
  27. try:
  28. if type_or_instance.__class__ in exclusion_list: # Check instance class
  29. return True
  30. except:
  31. pass
  32. return False
  33. def get_hash(args, trace_string, kwargs, ignore=UNDEFINED):
  34. counter_value = counter.get_from_trace_for_cache(trace_string)
  35. args_with_ignores = []
  36. kwargs_with_ignores = {}
  37. if ignore != UNDEFINED:
  38. for i, arg in enumerate(args):
  39. args_with_ignores.append(None if i in ignore.args else args[i])
  40. for k, v in kwargs.iteritems():
  41. kwargs_with_ignores[k] = None if k in ignore.kwargs else v
  42. args_with_ignores = tuple(args_with_ignores)
  43. else:
  44. args_with_ignores = args
  45. kwargs_with_ignores = kwargs
  46. return sha1((str(util.serialize_args(args_with_ignores)) + "\n" +
  47. str(counter_value) + "\n" +
  48. str(util.serialize_item(kwargs_with_ignores)) + "\n" +
  49. trace_string + "\n" )).hexdigest()
  50. class LazyBones:
  51. """
  52. A simple wrapper for lazy-loading arbitrary classes
  53. """
  54. def __init__(self, cls, args, kwargs):
  55. self.__class = cls
  56. self.__args = args
  57. self.__kwargs = kwargs
  58. def init(self):
  59. self.instance = self.__class( *self.__args, **self.__kwargs )
  60. return self.instance
  61. class Wrapper( dict ):
  62. """
  63. The Caliendo facade. Extends the Python object. Pass the initializer an object
  64. and the Facade will wrap all the public methods. Built-in methods
  65. (__somemethod__) and private methods (__somemethod) will not be copied. The
  66. Facade actually maintains a reference to the original object's methods so the
  67. state of that object is manipulated transparently as the Facade methods are
  68. called.
  69. """
  70. last_cached = None
  71. __exclusion_list = [ ]
  72. def wrapper__ignore(self, type_):
  73. """
  74. Selectively ignore certain types when wrapping attributes.
  75. :param class type: The class/type definition to ignore.
  76. :rtype list(type): The current list of ignored types
  77. """
  78. if type_ not in self.__exclusion_list:
  79. self.__exclusion_list.append(type_)
  80. return self.__exclusion_list
  81. def wrapper__unignore(self, type_):
  82. """
  83. Stop selectively ignoring certain types when wrapping attributes.
  84. :param class type: The class/type definition to stop ignoring.
  85. :rtype list(type): The current list of ignored types
  86. """
  87. if type_ in self.__exclusion_list:
  88. self.__exclusion_list.remove( type_ )
  89. return self.__exclusion_list
  90. def wrapper__delete_last_cached(self):
  91. """
  92. Deletes the last object that was cached by this instance of caliendo's Facade
  93. """
  94. return delete_io( self.last_cached )
  95. def wrapper__unwrap(self):
  96. """
  97. Returns the original object passed to the initializer for Wrapper
  98. :rtype mixed:
  99. """
  100. return self['__original_object']
  101. def __get_hash(self, args, trace_string, kwargs ):
  102. """
  103. Returns the hash from a trace string, args, and kwargs
  104. :param tuple args: The positional arguments to the function call
  105. :param str trace_string: The serialized stack trace for the function call
  106. :param dict kwargs: The keyword arguments to the function call
  107. :rtype str: The sha1 hashed result of the inputs plus a thuper-sthecial counter incremented in the local context of the call
  108. """
  109. return get_hash(args, trace_string, kwargs)
  110. def __cache( self, method_name, *args, **kwargs ):
  111. """
  112. Store a call descriptor
  113. """
  114. trace_string = util.get_stack(method_name)
  115. call_hash = self.__get_hash(args, trace_string, kwargs)
  116. cd = call_descriptor.fetch( call_hash )
  117. if not cd:
  118. c = self.__store__['callables'][method_name]
  119. if hasattr( c, '__class__' ) and c.__class__ == LazyBones:
  120. c = c.init()
  121. returnval = c(*args, **kwargs)
  122. cd = call_descriptor.CallDescriptor( hash = call_hash,
  123. stack = trace_string,
  124. method = method_name,
  125. returnval = returnval,
  126. args = args,
  127. kwargs = kwargs )
  128. cd.save()
  129. if not call_hash:
  130. raise Exception("CALL HASH IS NONE")
  131. util.last_hash = call_hash
  132. self.last_cached = call_hash
  133. else:
  134. returnval = cd.returnval
  135. if inspect.isclass(returnval):
  136. returnval = LazyBones( c, args, kwargs )
  137. return returnval
  138. def __wrap( self, method_name ):
  139. """
  140. This method actually does the wrapping. When it's given a method to copy it
  141. returns that method with facilities to log the call so it can be repeated.
  142. :param str method_name: The name of the method precisely as it's called on
  143. the object to wrap.
  144. :rtype lambda function:
  145. """
  146. return lambda *args, **kwargs: Facade( self.__cache( method_name, *args, **kwargs ), list(self.__exclusion_list) )
  147. def __getattr__( self, key ):
  148. if key not in self.__store__: # Attempt to lazy load the method (assuming __getattr__ is set on the incoming object)
  149. try:
  150. oo = self['__original_object']
  151. if hasattr( oo, '__class__' ) and oo.__class__ == LazyBones:
  152. oo = oo.init()
  153. val = eval( "oo." + key )
  154. self.__store_any(oo, key, val)
  155. except:
  156. raise Exception( "Key, " + str( key ) + " has not been set in the facade and failed to lazy load! Method is undefined." )
  157. val = self.__store__[key]
  158. if val and type(val) == tuple and val[0] == 'attr':
  159. return Facade(val[1])
  160. return self.__store__[ key ]
  161. def wrapper__get_store(self):
  162. """
  163. Returns the method/attribute store of the wrapper
  164. """
  165. return self.__store__
  166. def __store_callable(self, o, method_name, member):
  167. """
  168. Stores a callable member to the private __store__
  169. :param mixed o: Any callable (function or method)
  170. :param str method_name: The name of the attribute
  171. :param mixed member: A reference to the member
  172. """
  173. self.__store__['callables'][method_name] = eval( "o." + method_name )
  174. self.__store__['callables'][method_name[0].lower() + method_name[1:]] = eval( "o." + method_name )
  175. ret_val = self.__wrap( method_name )
  176. self.__store__[ method_name ] = ret_val
  177. self.__store__[ method_name[0].lower() + method_name[1:] ] = ret_val
  178. def __store_class(self, o, method_name, member):
  179. """
  180. Stores a class to the private __store__
  181. :param class o: The class to store
  182. :param str method_name: The name of the method
  183. :param class member: The actual class definition
  184. """
  185. self.__store__['callables'][method_name] = eval( "o." + method_name )
  186. self.__store__['callables'][method_name[0].lower() + method_name[1:]] = eval( "o." + method_name )
  187. ret_val = self.__wrap( method_name )
  188. self.__store__[ method_name ] = ret_val
  189. self.__store__[ method_name[0].lower() + method_name[1:] ] = ret_val
  190. def __store_nonprimitive(self, o, method_name, member):
  191. """
  192. Stores any 'non-primitive'. A primitive is in ( float, long, str, int, dict, list, unicode, tuple, set, frozenset, datetime.datetime, datetime.timedelta )
  193. :param mixed o: The non-primitive to store
  194. :param str method_name: The name of the attribute
  195. :param mixed member: The reference to the non-primitive
  196. """
  197. self.__store__[ method_name ] = ( 'attr', member )
  198. self.__store__[ method_name[0].lower() + method_name[1:] ] = ( 'attr', member )
  199. def __store_other(self, o, method_name, member):
  200. """
  201. Stores a reference to an attribute on o
  202. :param mixed o: Some object
  203. :param str method_name: The name of the attribute
  204. :param mixed member: The attribute
  205. """
  206. self.__store__[ method_name ] = eval( "o." + method_name )
  207. self.__store__[ method_name[0].lower() + method_name[1:] ] = eval( "o." + method_name )
  208. def __save_reference(self, o, cls, args, kwargs):
  209. """
  210. Saves a reference to the original object Facade is passed. This will either
  211. be the object itself or a LazyBones instance for lazy-loading later
  212. :param mixed o: The original object
  213. :param class cls: The class definition for the original object
  214. :param tuple args: The positional arguments to the original object
  215. :param dict kwargs: The keyword arguments to the original object
  216. """
  217. if not o and cls:
  218. self['__original_object'] = LazyBones( cls, args, kwargs )
  219. else:
  220. while hasattr( o, '__class__' ) and o.__class__ == Wrapper:
  221. o = o.wrapper__unwrap()
  222. self['__original_object'] = o
  223. def __store_any(self, o, method_name, member):
  224. """
  225. Determines type of member and stores it accordingly
  226. :param mixed o: Any parent object
  227. :param str method_name: The name of the method or attribuet
  228. :param mixed member: Any child object
  229. """
  230. if should_exclude( eval( "o." + method_name ), self.__exclusion_list ):
  231. self.__store__[ method_name ] = eval( "o." + method_name )
  232. return
  233. if hasattr( member, '__call__' ):
  234. self.__store_callable( o, method_name, member )
  235. elif inspect.isclass( member ):
  236. self.__store_class( o, method_name, member ) # Default ot lazy-loading classes here.
  237. elif not util.is_primitive( member ):
  238. self.__store_nonprimitive( o, method_name, member )
  239. else:
  240. self.__store_other( o, method_name, member )
  241. def __init__( self, o=None, exclusion_list=[], cls=None, args=tuple(), kwargs={} ):
  242. """
  243. The init method for the Wrapper class.
  244. :param mixed o: Some object to wrap.
  245. :param list exclusion_list: The list of types NOT to wrap
  246. :param class cls: The class definition for the object being mocked
  247. :param tuple args: The arguments for the class definition to return the desired instance
  248. :param dict kwargs: The keywork arguments for the class definition to return the desired instance
  249. """
  250. self.__store__ = {'callables': {}}
  251. self.__class = cls
  252. self.__args = args
  253. self.__kwargs = kwargs
  254. self.__exclusion_list = exclusion_list
  255. self.__save_reference(o, cls, args, kwargs)
  256. for method_name, member in inspect.getmembers(o):
  257. self.__store_any(o, method_name, member)
  258. try: # try-except because o is mixed type
  259. if o.wrapper__get_store: # For wrapping facades in a chain.
  260. store = o.wrapper__get_store()
  261. for key, val in store.items():
  262. if key == 'callables':
  263. self.__store__[key].update( val )
  264. self.__store__[key] = val
  265. except:
  266. pass
  267. def Facade( some_instance=None, exclusion_list=[], cls=None, args=tuple(), kwargs={} ):
  268. """
  269. Top-level interface to the Facade functionality. Determines what to return when passed arbitrary objects.
  270. :param mixed some_instance: Anything.
  271. :param list exclusion_list: The list of types NOT to wrap
  272. :param class cls: The class definition for the object being mocked
  273. :param tuple args: The arguments for the class definition to return the desired instance
  274. :param dict kwargs: The keywork arguments for the class definition to return the desired instance
  275. :rtype instance: Either the instance passed or an instance of the Wrapper wrapping the instance passed.
  276. """
  277. if not USE_CALIENDO or should_exclude( some_instance, exclusion_list ):
  278. if not util.is_primitive(some_instance):
  279. # Provide dummy methods to prevent errors in implementations dependent
  280. # on the Wrapper interface
  281. some_instance.wrapper__unwrap = lambda : None
  282. some_instance.wrapper__delete_last_cached = lambda : None
  283. return some_instance # Just give it back.
  284. else:
  285. if util.is_primitive(some_instance) and not cls:
  286. return some_instance
  287. return Wrapper(o=some_instance, exclusion_list=list(exclusion_list), cls=cls, args=args, kwargs=kwargs )
  288. def cache(handle=lambda *args, **kwargs: None, args=UNDEFINED, kwargs=UNDEFINED, ignore=UNDEFINED, call_stack=UNDEFINED, callback=UNDEFINED, subsequent_rvalue=UNDEFINED):
  289. """
  290. Store a call descriptor
  291. :param lambda handle: Any callable will work here. The method to cache.
  292. :param tuple args: The arguments to the method.
  293. :param dict kwargs: The keyword arguments to the method.
  294. :param tuple(list(int), list(str)) ignore: A tuple of arguments to ignore. The first element should be a list of positional arguments. The second should be a list of keys for keyword arguments.
  295. :param caliendo.hooks.CallStack call_stack: The stack of calls thus far for this patch.
  296. :param function callback: The callback function to execute each time there is a cache hit for 'handle' (actually mechanism is more complicated, but this is what it boils down to)
  297. :param mixed subsequent_rvalue: If passed; this will be the return value each time this method is run regardless of what is returned when it is initially cached. Caching for this method will be skipped. This is useful when the method returns something unpickleable but we still need to stub it out.
  298. :returns: The value of handle(*args, **kwargs)
  299. """
  300. if args == UNDEFINED:
  301. args = tuple()
  302. if kwargs == UNDEFINED:
  303. kwargs = {}
  304. if not USE_CALIENDO:
  305. return handle(*args, **kwargs)
  306. filtered_args = ignore.filter_args(args) if ignore is not UNDEFINED else args
  307. filtered_kwargs = ignore.filter_kwargs(kwargs) if ignore is not UNDEFINED else args
  308. trace_string = util.get_stack(handle.__name__)
  309. call_hash = get_hash(filtered_args, trace_string, filtered_kwargs, ignore)
  310. cd = call_descriptor.fetch(call_hash)
  311. modify_or_replace = 'no'
  312. util.set_current_hash(call_hash)
  313. if config.CALIENDO_PROMPT:
  314. display_name = ("(test %s): " % caliendo.util.current_test) if caliendo.util.current_test else ''
  315. if hasattr(handle, '__module__') and hasattr(handle, '__name__'):
  316. display_name += "%s.%s" % (handle.__module__, handle.__name__)
  317. else:
  318. display_name += handle
  319. if cd:
  320. modify_or_replace = prompt.should_modify_or_replace_cached(display_name)
  321. if not cd or modify_or_replace == 'replace':
  322. returnval = handle(*args, **kwargs)
  323. elif cd and modify_or_replace == 'modify':
  324. returnval = prompt.modify_cached_value(cd.returnval,
  325. calling_method=display_name,
  326. calling_test='')
  327. if cd and subsequent_rvalue != UNDEFINED:
  328. return subsequent_rvalue
  329. elif subsequent_rvalue != UNDEFINED:
  330. original_rvalue = returnval
  331. returnval = subsequent_rvalue
  332. if not cd or modify_or_replace != 'no':
  333. if isinstance(handle, types.MethodType):
  334. filtered_args = list(filtered_args)
  335. filtered_args[0] = util.serialize_item(filtered_args[0])
  336. filtered_args = tuple(filtered_args)
  337. cd = call_descriptor.CallDescriptor( hash = call_hash,
  338. stack = trace_string,
  339. method = handle.__name__,
  340. returnval = returnval,
  341. args = filtered_args,
  342. kwargs = filtered_kwargs )
  343. cd.save()
  344. util.set_last_hash(cd.hash)
  345. if call_stack != UNDEFINED:
  346. call_stack.add(cd)
  347. if callback != UNDEFINED:
  348. call_stack.add_hook(Hook(call_descriptor_hash=cd.hash,
  349. callback=callback))
  350. if subsequent_rvalue == UNDEFINED:
  351. return cd.returnval
  352. else:
  353. return original_rvalue
  354. def patch(*args, **kwargs):
  355. """
  356. Deprecated. Patch should now be imported from caliendo.patch.patch
  357. """
  358. from caliendo.patch import patch as p
  359. return p(*args, **kwargs)