From c8b047696e0b11f220b61345cc37f10a43d5639e Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sun, 1 Apr 2018 14:43:00 +0300 Subject: [PATCH 1/5] Avoid using __dict__ __dict__ is an implementation detail. We should avoid using it. Fixes #93. --- doubles/proxy_method.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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) From 6319b451bf991ad3e9d921edbe31e03f93dd1b77 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sun, 1 Apr 2018 18:34:09 +0300 Subject: [PATCH 2/5] Added tests. --- doubles/testing.py | 11 +++++++++++ test/class_double_test.py | 5 ++++- test/object_double_test.py | 11 ++++++----- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/doubles/testing.py b/doubles/testing.py index 4c11ec6..64ad85b 100644 --- a/doubles/testing.py +++ b/doubles/testing.py @@ -74,6 +74,17 @@ class User(OldStyleUser, object): pass +class UserWithSlots(User): + __slots__ = ( + 'class_attribute', + 'callable_class_attribute', + 'arbitrary_callable', + 'name', + 'age', + 'callable_instance_attribute' + ) + + 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) From 29d0c40434aceb3fa40a73eb4a905befe578d83c Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Mon, 2 Apr 2018 11:11:41 +0300 Subject: [PATCH 3/5] Add more tests. --- doubles/testing.py | 59 ++++++++++++++++++++++++++++++++++--- test/partial_double_test.py | 12 ++++---- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/doubles/testing.py b/doubles/testing.py index 64ad85b..df5e025 100644 --- a/doubles/testing.py +++ b/doubles/testing.py @@ -74,16 +74,67 @@ class User(OldStyleUser, object): pass -class UserWithSlots(User): +class UserWithSlots(object): __slots__ = ( - 'class_attribute', - 'callable_class_attribute', - 'arbitrary_callable', '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): 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') From 9783d28a4a892be385238f93a2840e16ce1e3fe8 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sat, 29 Sep 2018 16:58:25 +0300 Subject: [PATCH 4/5] Remove usage of __dict__. --- doubles/proxy_property.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) From faee21432d5926ac0b146ef734422c09e5ab8c39 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sat, 29 Sep 2018 17:07:41 +0300 Subject: [PATCH 5/5] Remove usage of __dict__. --- doubles/targets/allowance_target.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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)