from collections import namedtuple from inspect import classify_class_attrs, isclass, ismodule, getmembers from doubles.object_double import ObjectDouble from doubles.verification import is_callable Attribute = namedtuple('Attribute', ['object', 'kind', 'defining_class']) def _proxy_class_method_to_instance(original, name): def func(instance, *args, **kwargs): if name in instance.__dict__: return instance.__dict__[name](*args, **kwargs) return original(instance, *args, **kwargs) func._doubles_target_method = original return func class Target(object): """ A wrapper around an object that owns methods to be doubled. Provides additional introspection such as the class and the kind (method, class, property, etc.) """ def __init__(self, obj): """ :param object obj: The real target object. """ self.obj = obj self.doubled_obj = self._determine_doubled_obj() self.doubled_obj_type = self._determine_doubled_obj_type() self.attrs = self._generate_attrs() def is_class_or_module(self): """Determines if the object is a class or a module :return: True if the object is a class or a module, False otherwise. :rtype: bool """ if isinstance(self.obj, ObjectDouble): return self.obj.is_class return isclass(self.doubled_obj) or ismodule(self.doubled_obj) def _determine_doubled_obj(self): """Return the target object. Returns the object that should be treated as the target object. For partial doubles, this will be the same as ``self.obj``, but for pure doubles, it's pulled from the special ``_doubles_target`` attribute. :return: The object to be doubled. :rtype: object """ if isinstance(self.obj, ObjectDouble): return self.obj._doubles_target else: return self.obj def _determine_doubled_obj_type(self): """Returns the type (class) of the target object. :return: The type (class) of the target. :rtype: type, classobj """ if isclass(self.doubled_obj) or ismodule(self.doubled_obj): return self.doubled_obj return self.doubled_obj.__class__ def _generate_attrs(self): """Get detailed info about target object. Uses ``inspect.classify_class_attrs`` to get several important details about each attribute on the target object. :return: The attribute details dict. :rtype: dict """ attrs = {} if ismodule(self.doubled_obj): for name, func in getmembers(self.doubled_obj, is_callable): attrs[name] = Attribute(func, 'toplevel', self.doubled_obj) else: for attr in classify_class_attrs(self.doubled_obj_type): attrs[attr.name] = attr return attrs def hijack_attr(self, attr_name): """Hijack an attribute on the target object. Updates the underlying class and delegating the call to the instance. This allows specially-handled attributes like __call__, __enter__, and __exit__ to be mocked on a per-instance basis. :param str attr_name: the name of the attribute to hijack """ if not self._original_attr(attr_name): setattr( self.obj.__class__, attr_name, _proxy_class_method_to_instance( getattr(self.obj.__class__, attr_name, None), attr_name ), ) def restore_attr(self, attr_name): """Restore an attribute back onto the target object. :param str attr_name: the name of the attribute to restore """ original_attr = self._original_attr(attr_name) if self._original_attr(attr_name): setattr(self.obj.__class__, attr_name, original_attr) def _original_attr(self, attr_name): """Return the original attribute off of the proxy on the target object. :param str attr_name: the name of the original attribute to return :return: Func or None. :rtype: func """ try: return getattr( getattr(self.obj.__class__, attr_name), '_doubles_target_method', None ) except AttributeError: return None def get_callable_attr(self, attr_name): """Used to double methods added to an object after creation :param str attr_name: the name of the original attribute to return :return: Attribute or None. :rtype: func """ if not hasattr(self.doubled_obj, attr_name): return None func = getattr(self.doubled_obj, attr_name) if not is_callable(func): return None attr = Attribute( func, 'attribute', self.doubled_obj if self.is_class_or_module() else self.doubled_obj_type, ) self.attrs[attr_name] = attr return attr def get_attr(self, method_name): """Get attribute from the target object""" return self.attrs.get(method_name) or self.get_callable_attr(method_name)