From f9f52cf4098a733612e588f5079498b1628857d4 Mon Sep 17 00:00:00 2001 From: Charles-Axel Dein Date: Fri, 5 Oct 2018 16:35:51 +0200 Subject: [PATCH] Add support for inspect.signature --- doubles/proxy_method.py | 30 +++--- test/allow_test.py | 233 +++++++++++++++++++++------------------- 2 files changed, 143 insertions(+), 120 deletions(-) diff --git a/doubles/proxy_method.py b/doubles/proxy_method.py index 6606df6..c9f7f47 100644 --- a/doubles/proxy_method.py +++ b/doubles/proxy_method.py @@ -8,7 +8,7 @@ def double_name(name): - return 'double_of_' + name + return "double_of_" + name def _restore__new__(target, original_method): @@ -27,6 +27,7 @@ def _restore__new__(target, original_method): :param func original_method: The method to set __new__ to """ if isbuiltin(original_method): + @wraps(original_method) def _new(cls, *args, **kwargs): return original_method(cls) @@ -85,7 +86,7 @@ def __get__(self, instance, owner): :rtype: object, ProxyMethod """ - if self._attr.kind == 'property': + if self._attr.kind == "property": return self.__call__() return self @@ -98,25 +99,31 @@ def __name__(self): def __doc__(self): return self._original_method.__doc__ + @property + def __wrapped__(self): + return self._original_method + def restore_original_method(self): """Replaces the proxy method on the target object with its original value.""" if self._target.is_class_or_module(): setattr(self._target.obj, self._method_name, self._original_method) - if self._method_name == '__new__' and sys.version_info >= (3, 0): + if self._method_name == "__new__" and sys.version_info >= (3, 0): _restore__new__(self._target.obj, self._original_method) else: 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) + 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)] - elif self._attr.kind == 'attribute': + elif self._attr.kind == "attribute": self._target.obj.__dict__[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] - if self._method_name in ['__call__', '__enter__', '__exit__']: + if self._method_name in ["__call__", "__enter__", "__exit__"]: self._target.restore_attr(self._method_name) def _capture_original_method(self): @@ -129,17 +136,16 @@ def _hijack_target(self): if self._target.is_class_or_module(): setattr(self._target.obj, self._method_name, self) - elif self._attr.kind == 'property': + elif self._attr.kind == "property": proxy_property = ProxyProperty( - double_name(self._method_name), - self._original_method, + double_name(self._method_name), self._original_method ) setattr(self._target.obj.__class__, self._method_name, proxy_property) self._target.obj.__dict__[double_name(self._method_name)] = self else: self._target.obj.__dict__[self._method_name] = self - if self._method_name in ['__call__', '__enter__', '__exit__']: + if self._method_name in ["__call__", "__enter__", "__exit__"]: self._target.hijack_attr(self._method_name) def _raise_exception(self, args, kwargs): @@ -157,6 +163,6 @@ def _raise_exception(self, args, kwargs): error_message.format( self._method_name, self._target.obj, - build_argument_repr_string(args, kwargs) + build_argument_repr_string(args, kwargs), ) ) diff --git a/test/allow_test.py b/test/allow_test.py index 9f20b64..c7ee36c 100644 --- a/test/allow_test.py +++ b/test/allow_test.py @@ -1,3 +1,4 @@ +import inspect import re from pytest import raises @@ -5,7 +6,7 @@ from doubles.exceptions import ( UnallowedMethodCallError, MockExpectationError, - VerifyingDoubleArgumentError + VerifyingDoubleArgumentError, ) from doubles.instance_double import InstanceDouble from doubles import allow, no_builtin_verification @@ -19,47 +20,47 @@ class UserDefinedException(Exception): class TestBasicAllowance(object): def test_allows_method_calls_on_doubles(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method assert subject.instance_method() is None def test_raises_on_undefined_attribute_access(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") with raises(AttributeError): subject.instance_method def test_raises_if_called_with_args_that_do_not_match_signature(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method with raises(VerifyingDoubleArgumentError): - subject.instance_method('bar') + subject.instance_method("bar") def test_skip_builtin_verification_does_not_affect_non_builtins(self): with no_builtin_verification(): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method with raises(VerifyingDoubleArgumentError): - subject.instance_method('bar') + subject.instance_method("bar") def test_objects_with_custom__eq__method_one(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).method_with_positional_arguments.with_args(NeverEquals()) subject.method_with_positional_arguments(AlwaysEquals()) def test_objects_with_custom__eq__method_two(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).method_with_positional_arguments.with_args(AlwaysEquals()) subject.method_with_positional_arguments(NeverEquals()) def test_proxies_docstring(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).method_with_doc @@ -68,38 +69,46 @@ def test_proxies_docstring(self): ) def test_proxies_name(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).method_with_doc assert subject.method_with_doc.__name__ == "method_with_doc" + def test_proxies_can_be_inspected(self): + subject = InstanceDouble("doubles.testing.User") + + allow(subject).instance_method + + parameters = inspect.signature(subject.instance_method).parameters + assert len(parameters) == 1 + class TestWithArgs(object): def test__call__is_short_hand_for_with_args(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") - allow(subject).method_with_positional_arguments('Bob').and_return('Barker') - assert subject.method_with_positional_arguments('Bob') == 'Barker' + allow(subject).method_with_positional_arguments("Bob").and_return("Barker") + assert subject.method_with_positional_arguments("Bob") == "Barker" def test_allows_any_arguments_if_none_are_specified(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") - allow(subject).method_with_positional_arguments.and_return('bar') + allow(subject).method_with_positional_arguments.and_return("bar") - assert subject.method_with_positional_arguments('unspecified argument') == 'bar' + assert subject.method_with_positional_arguments("unspecified argument") == "bar" def test_allows_specification_of_arguments(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") - allow(subject).method_with_positional_arguments.with_args('foo') + allow(subject).method_with_positional_arguments.with_args("foo") - assert subject.method_with_positional_arguments('foo') is None + assert subject.method_with_positional_arguments("foo") is None def test_raises_if_arguments_were_specified_but_not_provided_when_called(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") - allow(subject).method_with_default_args.with_args('one', bar='two') + allow(subject).method_with_default_args.with_args("one", bar="two") with raises(UnallowedMethodCallError) as e: subject.method_with_default_args() @@ -110,16 +119,16 @@ def test_raises_if_arguments_were_specified_but_not_provided_when_called(self): r"(?: at 0x[0-9a-f]{9})?> object at .+>\." r" The supplied arguments \(\)" r" do not match any available allowances.", - str(e.value) + str(e.value), ) def test_raises_if_arguments_were_specified_but_wrong_kwarg_used_when_called(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") - allow(subject).method_with_default_args.with_args('one', bar='two') + allow(subject).method_with_default_args.with_args("one", bar="two") with raises(UnallowedMethodCallError) as e: - subject.method_with_default_args('one', bob='barker') + subject.method_with_default_args("one", bob="barker") assert re.match( r"Received unexpected call to 'method_with_default_args' on " @@ -127,16 +136,16 @@ def test_raises_if_arguments_were_specified_but_wrong_kwarg_used_when_called(sel r"(?: at 0x[0-9a-f]{9})?> object at .+>\." r" The supplied arguments \('one', bob='barker'\)" r" do not match any available allowances.", - str(e.value) + str(e.value), ) def test_raises_if_method_is_called_with_wrong_arguments(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") - allow(subject).method_with_varargs.with_args('bar') + allow(subject).method_with_varargs.with_args("bar") with raises(UnallowedMethodCallError) as e: - subject.method_with_varargs('baz') + subject.method_with_varargs("baz") assert re.match( r"Received unexpected call to 'method_with_varargs' on " @@ -144,33 +153,33 @@ def test_raises_if_method_is_called_with_wrong_arguments(self): r"(?: at 0x[0-9a-f]{9})?> object at .+>\." r" The supplied arguments \('baz'\)" r" do not match any available allowances.", - str(e.value) + str(e.value), ) def test_matches_most_specific_allowance(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") - allow(subject).method_with_varargs.and_return('bar') - allow(subject).method_with_varargs.with_args('baz').and_return('blah') + allow(subject).method_with_varargs.and_return("bar") + allow(subject).method_with_varargs.with_args("baz").and_return("blah") - assert subject.method_with_varargs('baz') == 'blah' + assert subject.method_with_varargs("baz") == "blah" class TestWithNoArgs(object): def test_allows_call_with_no_arguments(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.with_no_args() assert subject.instance_method() is None def test_raises_if_called_with_args(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.with_no_args() with raises(UnallowedMethodCallError) as e: - subject.instance_method('bar') + subject.instance_method("bar") assert re.match( r"Received unexpected call to 'instance_method' on " @@ -178,20 +187,20 @@ def test_raises_if_called_with_args(self): r"(?: at 0x[0-9a-f]{9})?> object at .+>\." r" The supplied arguments \('bar'\)" r" do not match any available allowances.", - str(e.value) + str(e.value), ) def test_chains_with_return_values(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") - allow(subject).instance_method.with_no_args().and_return('bar') + allow(subject).instance_method.with_no_args().and_return("bar") - assert subject.instance_method() == 'bar' + assert subject.instance_method() == "bar" class TestTwice(object): def test_passes_when_called_twice(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.twice() @@ -199,14 +208,14 @@ def test_passes_when_called_twice(self): subject.instance_method() def test_passes_when_called_once(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.twice() subject.instance_method() def test_fails_when_called_three_times(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.twice() @@ -221,20 +230,20 @@ def test_fails_when_called_three_times(self): r" object at .+> " r"with any args, but was not." r" \(.*doubles/test/allow_test.py:\d+\)", - str(e.value) + str(e.value), ) class TestOnce(object): def test_passes_when_called_once(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.once() subject.instance_method() def test_fails_when_called_two_times(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.once() @@ -248,18 +257,18 @@ def test_fails_when_called_two_times(self): r" object at .+> " r"with any args, but was not." r" \(.*doubles/test/allow_test.py:\d+\)", - str(e.value) + str(e.value), ) class TestZeroTimes(object): def test_passes_when_called_never(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.never() def test_fails_when_called_once_times(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.never() @@ -272,25 +281,22 @@ def test_fails_when_called_once_times(self): r"time on " r"object at .+> with any args, but was not." r" \(.*doubles/test/allow_test.py:\d+\)", - str(e.value) + str(e.value), ) class TestExactly(object): def test_raises_if_called_with_negative_value(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") with raises(TypeError) as e: allow(subject).instance_method.exactly(-1).times teardown() - assert re.match( - r"exactly requires one positive integer argument", - str(e.value) - ) + assert re.match(r"exactly requires one positive integer argument", str(e.value)) def test_called_with_zero(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.exactly(0).times @@ -303,11 +309,11 @@ def test_called_with_zero(self): r"time on " r"object at .+> with any args, but was not." r" \(.*doubles/test/allow_test.py:\d+\)", - str(e.value) + str(e.value), ) def test_calls_are_chainable(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.exactly(1).time.exactly(2).times @@ -315,21 +321,21 @@ def test_calls_are_chainable(self): subject.instance_method() def test_passes_when_called_less_than_expected_times(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.exactly(2).times subject.instance_method() def test_passes_when_called_exactly_expected_times(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.exactly(1).times subject.instance_method() def test_fails_when_called_more_than_expected_times(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.exactly(1).times @@ -343,44 +349,43 @@ def test_fails_when_called_more_than_expected_times(self): r" object at .+> " r"with any args, but was not." r" \(.*doubles/test/allow_test.py:\d+\)", - str(e.value) + str(e.value), ) class TestAtLeast(object): def test_raises_if_called_with_negative_value(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") with raises(TypeError) as e: allow(subject).instance_method.at_least(-1).times teardown() assert re.match( - r"at_least requires one positive integer argument", - str(e.value) + r"at_least requires one positive integer argument", str(e.value) ) def test_if_called_with_zero(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.at_least(0).times def test_calls_are_chainable(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.at_least(2).times.at_least(1).times subject.instance_method() def test_passes_when_called_less_than_at_least_times(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.at_least(2).times subject.instance_method() def test_passes_when_called_exactly_at_least_times(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.at_least(1).times @@ -388,7 +393,7 @@ def test_passes_when_called_exactly_at_least_times(self): subject.instance_method() def test_passes_when_called_more_than_at_least_times(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.at_least(1).times @@ -398,24 +403,21 @@ def test_passes_when_called_more_than_at_least_times(self): class TestAtMost(object): def test_raises_if_called_with_negative_value(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") with raises(TypeError) as e: allow(subject).instance_method.at_most(-1).times teardown() - assert re.match( - r"at_most requires one positive integer argument", - str(e.value) - ) + assert re.match(r"at_most requires one positive integer argument", str(e.value)) def test_called_with_zero(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.at_most(0).times def test_calls_are_chainable(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.at_most(1).times.at_most(2).times @@ -423,21 +425,21 @@ def test_calls_are_chainable(self): subject.instance_method() def test_passes_when_called_exactly_at_most_times(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.at_most(1).times subject.instance_method() def test_passes_when_called_less_than_at_most_times(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.at_most(2).times subject.instance_method() def test_fails_when_called_more_than_at_most_times(self): - subject = InstanceDouble('doubles.testing.User') + subject = InstanceDouble("doubles.testing.User") allow(subject).instance_method.at_most(1).times @@ -451,25 +453,27 @@ def test_fails_when_called_more_than_at_most_times(self): r" object at .+> " r"with any args, but was not." r" \(.*doubles/test/allow_test.py:\d+\)", - str(e.value) + str(e.value), ) class TestCustomMatcher(object): def setup(self): - self.subject = InstanceDouble('doubles.testing.User') + self.subject = InstanceDouble("doubles.testing.User") def test_matcher_raises_an_exception(self): def func(): - raise Exception('Bob Barker') + raise Exception("Bob Barker") allow(self.subject).instance_method.with_args_validator(func) with raises(UnallowedMethodCallError): self.subject.instance_method() def test_matcher_with_no_args_returns_true(self): - allow(self.subject).instance_method.with_args_validator(lambda: True).and_return('Bob') - self.subject.instance_method() == 'Bob' + allow(self.subject).instance_method.with_args_validator( + lambda: True + ).and_return("Bob") + self.subject.instance_method() == "Bob" def test_matcher_with_no_args_returns_false(self): allow(self.subject).instance_method.with_args_validator(lambda: False) @@ -477,20 +481,24 @@ def test_matcher_with_no_args_returns_false(self): self.subject.instance_method() def test_matcher_with_positional_args_returns_true(self): - (allow(self.subject) - .method_with_positional_arguments - .with_args_validator(lambda x: True) - .and_return('Bob')) - self.subject.method_with_positional_arguments('Bob Barker') == 'Bob' + ( + allow(self.subject) + .method_with_positional_arguments.with_args_validator(lambda x: True) + .and_return("Bob") + ) + self.subject.method_with_positional_arguments("Bob Barker") == "Bob" def test_matcher_with_positional_args_returns_false(self): - allow(self.subject).method_with_positional_arguments.with_args_validator(lambda x: False) + allow(self.subject).method_with_positional_arguments.with_args_validator( + lambda x: False + ) with raises(UnallowedMethodCallError): - self.subject.method_with_positional_arguments('Bob Barker') + self.subject.method_with_positional_arguments("Bob Barker") def test_matcher_with_kwargs_args_returns_false(self): def func(bar=None): return False + allow(self.subject).instance_method.with_args_validator(func) with raises(UnallowedMethodCallError): self.subject.instance_method() @@ -498,48 +506,57 @@ def func(bar=None): def test_matcher_with_kwargs_args_returns_true(self): def func(bar=None): return True - allow(self.subject).instance_method.with_args_validator(func).and_return('Bob') - self.subject.instance_method() == 'Bob' + + allow(self.subject).instance_method.with_args_validator(func).and_return("Bob") + self.subject.instance_method() == "Bob" def test_matcher_with_positional_and_kwargs_returns_true(self): def func(foo, bar=None): return True - allow(self.subject).method_with_default_args.with_args_validator(func).and_return('Bob') - self.subject.method_with_default_args('bob', bar='Barker') == 'Bob' + + allow(self.subject).method_with_default_args.with_args_validator( + func + ).and_return("Bob") + self.subject.method_with_default_args("bob", bar="Barker") == "Bob" def test_matcher_with_positional_and_kwargs_returns_false(self): def func(foo, bar=None): return False - allow(self.subject).method_with_default_args.with_args_validator(func).and_return('Bob') + + allow(self.subject).method_with_default_args.with_args_validator( + func + ).and_return("Bob") with raises(UnallowedMethodCallError): - self.subject.method_with_default_args('bob', bar='Barker') + self.subject.method_with_default_args("bob", bar="Barker") def test_matcher_returns_true_but_args_do_not_match_call_signature(self): allow(self.subject).instance_method.with_args_validator(lambda x: True) with raises(VerifyingDoubleArgumentError): - self.subject.instance_method('bob') + self.subject.instance_method("bob") class TestAsync(object): def setup(self): - self.subject = InstanceDouble('doubles.testing.User') + self.subject = InstanceDouble("doubles.testing.User") def test_and_return_future(self): - allow(self.subject).instance_method.and_return_future('Bob Barker') + allow(self.subject).instance_method.and_return_future("Bob Barker") result = self.subject.instance_method() - assert result.result() == 'Bob Barker' + assert result.result() == "Bob Barker" def test_and_return_future_multiple_values(self): - allow(self.subject).instance_method.and_return_future('Bob Barker', 'Drew Carey') + allow(self.subject).instance_method.and_return_future( + "Bob Barker", "Drew Carey" + ) result1 = self.subject.instance_method() result2 = self.subject.instance_method() - assert result1.result() == 'Bob Barker' - assert result2.result() == 'Drew Carey' + assert result1.result() == "Bob Barker" + assert result2.result() == "Drew Carey" def test_and_raise_future(self): - exception = Exception('Bob Barker') + exception = Exception("Bob Barker") allow(self.subject).instance_method.and_raise_future(exception) result = self.subject.instance_method()