diff --git a/doubles/proxy_method.py b/doubles/proxy_method.py index a38e022..5095675 100644 --- a/doubles/proxy_method.py +++ b/doubles/proxy_method.py @@ -109,12 +109,12 @@ def restore_original_method(self): setattr(self._target.obj, self._method_name, self._original_method) elif self._attr.kind == 'property': setattr(self._target.obj.__class__, self._method_name, self._original_method) - del self._target.obj.__dict__[double_name(self._method_name)] + delattr(self._target.obj, double_name(self._method_name)) elif self._attr.kind == 'attribute': - self._target.obj.__dict__[self._method_name] = self._original_method + setattr(self._target.obj, self._method_name, self._original_method) else: # TODO: Could there ever have been a value here that needs to be restored? - del self._target.obj.__dict__[self._method_name] + delattr(self._target.obj, self._method_name) if self._method_name in ['__call__', '__enter__', '__exit__']: self._target.restore_attr(self._method_name) @@ -135,9 +135,9 @@ def _hijack_target(self): self._original_method, ) setattr(self._target.obj.__class__, self._method_name, proxy_property) - self._target.obj.__dict__[double_name(self._method_name)] = self + setattr(self._target.obj, double_name(self._method_name), self) else: - self._target.obj.__dict__[self._method_name] = self + setattr(self._target.obj, self._method_name, self) if self._method_name in ['__call__', '__enter__', '__exit__']: self._target.hijack_attr(self._method_name) diff --git a/doubles/proxy_property.py b/doubles/proxy_property.py index 2a76feb..33bf860 100644 --- a/doubles/proxy_property.py +++ b/doubles/proxy_property.py @@ -8,6 +8,6 @@ def __init__(self, name, original): self._original = original def __get__(self, obj, objtype=None): - if self._name in obj.__dict__: - return obj.__dict__[self._name].__get__(obj, objtype) + if hasattr(obj, self._name): + return getattr(obj, self._name).__get__(obj, objtype) return self._original.__get__(obj, objtype) diff --git a/doubles/targets/allowance_target.py b/doubles/targets/allowance_target.py index f18288e..19b578b 100644 --- a/doubles/targets/allowance_target.py +++ b/doubles/targets/allowance_target.py @@ -60,10 +60,8 @@ def __getattribute__(self, attr_name): :rtype: object, Allowance """ - __dict__ = object.__getattribute__(self, '__dict__') - - if __dict__ and attr_name in __dict__: - return __dict__[attr_name] - - caller = inspect.getframeinfo(inspect.currentframe().f_back) - return self._proxy.add_allowance(attr_name, caller) + try: + return object.__getattribute__(self, attr_name) + except AttributeError: + caller = inspect.getframeinfo(inspect.currentframe().f_back) + return self._proxy.add_allowance(attr_name, caller) diff --git a/doubles/testing.py b/doubles/testing.py index 4c11ec6..df5e025 100644 --- a/doubles/testing.py +++ b/doubles/testing.py @@ -74,6 +74,68 @@ class User(OldStyleUser, object): pass +class UserWithSlots(object): + __slots__ = ( + 'name', + 'age', + 'callable_instance_attribute' + ) + + """An importable dummy class used for testing purposes.""" + + class_attribute = 'foo' + callable_class_attribute = classmethod(lambda cls: 'dummy result') + arbitrary_callable = ArbitraryCallable('ArbitraryCallable Value') + + def __init__(self, name, age): + self.name = name + self.age = age + self.callable_instance_attribute = lambda: 'dummy result' + + @staticmethod + def static_method(arg): + return 'static_method return value: {}'.format(arg) + + @classmethod + def class_method(cls, arg): + return 'class_method return value: {}'.format(arg) + + def get_name(self): + return self.name + + def instance_method(self): + return 'instance_method return value' + + def method_with_varargs(self, *args): + return 'method_with_varargs return value' + + def method_with_default_args(self, foo, bar='baz'): + return 'method_with_default_args return value' + + def method_with_varkwargs(self, **kwargs): + return 'method_with_varkwargs return value' + + def method_with_positional_arguments(self, foo): + return 'method_with_positional_arguments return value' + + def method_with_doc(self): + """A basic method of UserWithSlots to illustrate existance of a docstring""" + return + + @property + def some_property(self): + return 'some_property return value' + + def __call__(self, *args): + return 'user was called' + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + class UserWithCustomNew(User): def __new__(cls, name, age): instance = User.__new__(cls) diff --git a/test/class_double_test.py b/test/class_double_test.py index 1593121..546a6a8 100644 --- a/test/class_double_test.py +++ b/test/class_double_test.py @@ -14,6 +14,7 @@ from doubles.testing import ( User, OldStyleUser, + UserWithSlots, EmptyClass, OldStyleEmptyClass, ) @@ -21,6 +22,7 @@ TEST_CLASSES = ( 'doubles.testing.User', 'doubles.testing.OldStyleUser', + 'doubles.testing.UserWithSlots', 'doubles.testing.EmptyClass', 'doubles.testing.OldStyleEmptyClass', ) @@ -28,6 +30,7 @@ VALID_ARGS = { 'doubles.testing.User': ('Bob', 100), 'doubles.testing.OldStyleUser': ('Bob', 100), + 'doubles.testing.UserWithSlots': ('Bob', 100), 'doubles.testing.EmptyClass': tuple(), 'doubles.testing.OldStyleEmptyClass': tuple(), } @@ -166,7 +169,7 @@ def test_with_valid_args(self, test_class): assert TestClass(*VALID_ARGS[test_class]) is None -@mark.parametrize('test_class', [User, OldStyleUser, EmptyClass, OldStyleEmptyClass]) +@mark.parametrize('test_class', [User, OldStyleUser, UserWithSlots, EmptyClass, OldStyleEmptyClass]) class TestingStubbingNonClassDoubleConstructors(object): def test_raises_if_you_allow_constructor(self, test_class): with raises(ConstructorDoubleError): diff --git a/test/object_double_test.py b/test/object_double_test.py index db92af5..28607ba 100644 --- a/test/object_double_test.py +++ b/test/object_double_test.py @@ -5,26 +5,27 @@ from doubles.exceptions import VerifyingDoubleArgumentError, VerifyingDoubleError from doubles.object_double import ObjectDouble from doubles.targets.allowance_target import allow -from doubles.testing import User, OldStyleUser +from doubles.testing import User, OldStyleUser, UserWithSlots user = User('Alice', 25) old_style_user = OldStyleUser('Alice', 25) +user_with_slots = UserWithSlots('Alice', 25) -@mark.parametrize('test_object', [user, old_style_user]) +@mark.parametrize('test_object', [user, old_style_user, user_with_slots]) class TestRepr(object): def test_displays_correct_class_name(self, test_object): subject = ObjectDouble(test_object) assert re.match( - r" object " - r"at 0x[0-9a-f]+>", + r"at 0x[0-9a-f]+>" % test_object.__class__.__name__, repr(subject) ) -@mark.parametrize('test_object', [user, old_style_user]) +@mark.parametrize('test_object', [user, old_style_user, user_with_slots]) class TestObjectDouble(object): def test_allows_stubs_on_existing_methods(self, test_object): doubled_user = ObjectDouble(test_object) diff --git a/test/partial_double_test.py b/test/partial_double_test.py index 46719c1..901ca47 100644 --- a/test/partial_double_test.py +++ b/test/partial_double_test.py @@ -7,11 +7,11 @@ ) from doubles.lifecycle import teardown from doubles import allow, no_builtin_verification -from doubles.testing import User, OldStyleUser, UserWithCustomNew +from doubles.testing import User, OldStyleUser, UserWithSlots, UserWithCustomNew import doubles.testing -@mark.parametrize('test_class', [User, OldStyleUser]) +@mark.parametrize('test_class', [User, OldStyleUser, UserWithSlots]) class TestInstanceMethods(object): def test_arbitrary_callable_on_instance(self, test_class): instance = test_class('Bob', 10) @@ -121,7 +121,7 @@ def test_teardown_restores_properties(self, test_class): assert user_2.some_property == 'some_property return value' -@mark.parametrize('test_class', [User, OldStyleUser]) +@mark.parametrize('test_class', [User, OldStyleUser, UserWithSlots]) class Test__call__(object): def test_basic_usage(self, test_class): user = test_class('Alice', 25) @@ -177,7 +177,7 @@ def test_raises_when_mocked_with_invalid_call_signature(self, test_class): allow(user).__call__.with_args(1, 2, bob='barker') -@mark.parametrize('test_class', [User, OldStyleUser]) +@mark.parametrize('test_class', [User, OldStyleUser, UserWithSlots]) class Test__enter__(object): def test_basic_usage(self, test_class): user = test_class('Alice', 25) @@ -229,7 +229,7 @@ def test_raises_when_mocked_with_invalid_call_signature(self, test_class): allow(user).__enter__.with_args(1) -@mark.parametrize('test_class', [User, OldStyleUser]) +@mark.parametrize('test_class', [User, OldStyleUser, UserWithSlots]) class Test__exit__(object): def test_basic_usage(self, test_class): user = test_class('Alice', 25) @@ -273,7 +273,7 @@ def test_raises_when_mocked_with_invalid_call_signature(self, test_class): allow(user).__exit__.with_no_args() -@mark.parametrize('test_class', [User, OldStyleUser]) +@mark.parametrize('test_class', [User, OldStyleUser, UserWithSlots]) class TestClassMethods(object): def test_stubs_class_methods(self, test_class): allow(test_class).class_method.with_args('foo').and_return('overridden value')