Skip to content

Commit 728efce

Browse files
committed
bpo-30587: Extend autospec test coverage and fix partial / spec-list gaps
Fix _get_signature_object to handle functools.partial specs via inspect.signature() directly, instead of going through partial.__call__. This was silently disabling signature checking for any partial function / method spec. Preserve an explicit spec given as a list of attribute names alongside autospec, extending the autospecced attribute set instead of letting autospec override it entirely. This allows whitelisting instance attributes that are only set in __init__, which are invisible to autospec. Add tests for: - autospec combined with wraps (sync and async). - autospec on plain functions, partial functions and partial methods. - a function side_effect on an autospec'd method. - a class's __init__ signature being enforced. - autospec + spec list attribute whitelist combination. - a real @Property descriptor is not eagerly triggered by autospec=, mirroring the existing spec= test. - autospec propagates recursively through a class-level attribute that is itself a spec'd object, enforcing the inner object's signatures too. - reset_mock() does not clear _mock_check_sig. - attach_mock() with a Mock(autospec=...) child preserves both call recording and signature enforcement after reparenting. - magic / dunder methods on a MagicMock(autospec=...): existence is still gated by the spec, and __call__'s signature is enforced (against __init__ when autospeccing a class, against __call__ itself when autospeccing an instance). Signed-off-by: Claudiu Belu <cbelu@cloudbasesolutions.com>
1 parent aa58556 commit 728efce

2 files changed

Lines changed: 208 additions & 6 deletions

File tree

Lib/test/test_unittest/testmock/testmock.py

Lines changed: 189 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import copy
3+
import functools
34
import re
45
import sys
56
import tempfile
@@ -30,7 +31,8 @@ def next(self):
3031

3132

3233
class Something(object):
33-
def meth(self, a, b, c, d=None): pass
34+
def meth(self, a, b, c, d=None):
35+
return a, b, c, d
3436

3537
@classmethod
3638
def cmeth(cls, a, b, c, d=None): pass
@@ -66,7 +68,21 @@ class Typos():
6668
set_spec = None
6769

6870

69-
def something(a): pass
71+
def something(a):
72+
return a
73+
74+
75+
def something_two_args(a, b):
76+
return a, b
77+
78+
79+
class SomethingWithProps(object):
80+
def __init__(self, a, b, c=None):
81+
self.a = a
82+
self.b = b
83+
self.c = c
84+
85+
def meth(self, x, y): pass
7086

7187

7288
class MockTest(unittest.TestCase):
@@ -382,6 +398,17 @@ def test_reset_mock(self):
382398
"children incorrectly cleared")
383399
self.assertFalse(mock.something.called, "child not reset")
384400

401+
def test_mock_autospec_reset_mock(self):
402+
mock_something = Mock(autospec=Something)
403+
mock_something.meth(sentinel.a, sentinel.b, sentinel.c)
404+
405+
mock_something.reset_mock()
406+
407+
self.assertEqual(mock_something.meth.call_count, 0)
408+
# the child mock is preserved by reset_mock(), so signature checking
409+
# is still enforced afterwards.
410+
self.assertRaises(TypeError, mock_something.meth)
411+
mock_something.meth(sentinel.a, sentinel.b, sentinel.c)
385412

386413
def test_reset_mock_recursion(self):
387414
mock = Mock()
@@ -738,6 +765,12 @@ def test_mock_autospec_all_members(self):
738765
self._check_autospeced_something(mock_something)
739766

740767

768+
def test_mock_autospec_all_members_wraps(self):
769+
something = Something()
770+
mock_something = Mock(autospec=something, wraps=something)
771+
self._check_autospeced_something(mock_something)
772+
773+
741774
def _check_autospeced_something_async(self, something):
742775
# assert that AttributeError is raised if the method does not exist.
743776
self.assertRaises(AttributeError, getattr, something, 'foolish')
@@ -772,6 +805,140 @@ def test_mock_autospec_all_members_async(self):
772805
self._check_autospeced_something_async(mock_something)
773806

774807

808+
@requires_working_socket()
809+
def test_mock_autospec_all_members_wraps_async(self):
810+
something = SomethingAsync()
811+
mock_something = AsyncMock(autospec=something, wraps=something)
812+
self._check_autospeced_something_async(mock_something)
813+
814+
815+
def test_mock_autospec_function(self):
816+
mock_func = Mock(autospec=something, wraps=something)
817+
818+
result = mock_func(sentinel.a)
819+
self.assertEqual(result, sentinel.a)
820+
821+
self.assertRaises(TypeError, mock_func)
822+
self.assertRaises(TypeError, mock_func, sentinel.a, sentinel.b)
823+
824+
825+
def test_mock_autospec_partial_function(self):
826+
partial_something = functools.partial(something_two_args, sentinel.a)
827+
828+
mock_func = Mock(autospec=partial_something, wraps=partial_something)
829+
830+
result = mock_func(sentinel.b)
831+
self.assertEqual(result, (sentinel.a, sentinel.b))
832+
833+
self.assertRaises(TypeError, mock_func)
834+
self.assertRaises(TypeError, mock_func, sentinel.b, sentinel.c)
835+
836+
837+
def test_mock_autospec_partial_method(self):
838+
obj = Something()
839+
partial_meth = functools.partial(obj.meth, sentinel.a, sentinel.b)
840+
841+
mock_meth = Mock(autospec=partial_meth, wraps=partial_meth)
842+
843+
result = mock_meth(sentinel.c)
844+
self.assertEqual(result, (sentinel.a, sentinel.b, sentinel.c, None))
845+
846+
self.assertRaises(TypeError, mock_meth)
847+
self.assertRaises(TypeError, mock_meth, sentinel.d, e=sentinel.e)
848+
849+
850+
def test_mock_autospec_side_effect(self):
851+
def side_effect(a, b, c, d=None):
852+
return (a, b, c, d)
853+
854+
mock_something = Mock(autospec=Something)
855+
mock_something.meth.side_effect = side_effect
856+
857+
result = mock_something.meth(sentinel.a, sentinel.b, sentinel.c)
858+
self.assertEqual(result, (sentinel.a, sentinel.b, sentinel.c, None))
859+
860+
# signature checking is enforced before the side_effect runs.
861+
self.assertRaises(TypeError, mock_something.meth)
862+
self.assertRaises(TypeError, mock_something.meth, sentinel.a)
863+
864+
865+
def test_mock_autospec_class_init_signature(self):
866+
mock_class = Mock(autospec=SomethingWithProps)
867+
868+
mock_class(sentinel.a, sentinel.b)
869+
mock_class(sentinel.a, sentinel.b, sentinel.c)
870+
871+
self.assertRaises(TypeError, mock_class)
872+
self.assertRaises(TypeError, mock_class, sentinel.a)
873+
self.assertRaises(TypeError, mock_class, sentinel.a, sentinel.b,
874+
sentinel.c, e=sentinel.e)
875+
876+
877+
def test_mock_autospec_with_spec_list_for_init_only_attributes(self):
878+
# SomethingWithProps only assigns 'a' and 'b' as instance attributes
879+
# in __init__, so they are absent from dir(SomethingWithProps) and
880+
# would normally be rejected by autospec.
881+
mock_something = Mock(
882+
autospec=SomethingWithProps, spec=['a', 'b', 'c'])
883+
884+
# the extra attributes from `spec` are accessible.
885+
mock_something.a
886+
mock_something.b
887+
888+
# autospec is still applied: methods are still signature-checked.
889+
mock_something.meth(sentinel.x, sentinel.y)
890+
self.assertRaises(TypeError, mock_something.meth)
891+
892+
# attributes that are neither on the spec class nor in the extra
893+
# `spec` list are still rejected.
894+
self.assertRaises(AttributeError, getattr, mock_something, 'foolish')
895+
896+
897+
def test_mock_autospec_nested(self):
898+
class Inner(object):
899+
def meth(self, a, b): pass
900+
901+
class Outer(object):
902+
inner = Inner()
903+
904+
mock_outer = Mock(autospec=Outer)
905+
self.assertIsInstance(mock_outer.inner, Mock)
906+
907+
mock_outer.inner.meth(sentinel.a, sentinel.b)
908+
self.assertRaises(TypeError, mock_outer.inner.meth)
909+
self.assertRaises(TypeError, mock_outer.inner.meth, sentinel.a)
910+
911+
912+
def test_mock_autospec_magic_methods(self):
913+
for Klass in MagicMock, NonCallableMagicMock:
914+
mock = Klass(autospec=int)
915+
int(mock)
916+
917+
mock.__int__.return_value = 4
918+
self.assertEqual(int(mock), 4)
919+
920+
# attributes that don't exist on the spec are still rejected.
921+
self.assertRaises(AttributeError, getattr, mock, 'foo')
922+
923+
924+
def test_mock_autospec_call_signature(self):
925+
class Caller(object):
926+
def __init__(self, a): pass
927+
def __call__(self, x): pass
928+
929+
# autospeccing the class checks the constructor signature.
930+
mock_cls = MagicMock(autospec=Caller)
931+
mock_cls(a=sentinel.a)
932+
self.assertRaises(TypeError, mock_cls)
933+
self.assertRaises(TypeError, mock_cls, sentinel.a, sentinel.b)
934+
935+
# autospeccing an instance checks the __call__ signature instead.
936+
mock_instance = MagicMock(autospec=Caller(sentinel.a))
937+
mock_instance(x=sentinel.x)
938+
self.assertRaises(TypeError, mock_instance)
939+
self.assertRaises(TypeError, mock_instance, sentinel.x, sentinel.y)
940+
941+
775942
def test_wraps_calls(self):
776943
real = Mock()
777944

@@ -2262,6 +2429,19 @@ def test_attach_mock_return_value(self):
22622429
self.assertEqual(m.mock_calls, call().foo().call_list())
22632430

22642431

2432+
def test_mock_autospec_attach_mock(self):
2433+
m = Mock()
2434+
child = Mock(autospec=Something)
2435+
m.attach_mock(child, 'child')
2436+
2437+
child.meth(sentinel.a, sentinel.b, sentinel.c)
2438+
m.assert_has_calls(
2439+
[call.child.meth(sentinel.a, sentinel.b, sentinel.c)])
2440+
2441+
# signature checking survives being attached to another mock.
2442+
self.assertRaises(TypeError, child.meth)
2443+
2444+
22652445
def test_attach_mock_patch_autospec(self):
22662446
parent = Mock()
22672447

@@ -2511,6 +2691,13 @@ def test_property_not_called_with_spec_mock(self):
25112691
self.assertIsNone(obj._instance, msg='after mock')
25122692
self.assertEqual('object', obj.instance)
25132693

2694+
def test_mock_autospec_property_not_called(self):
2695+
obj = SomethingElse()
2696+
self.assertIsNone(obj._instance, msg='before mock')
2697+
mock = Mock(autospec=obj)
2698+
self.assertIsNone(obj._instance)
2699+
self.assertEqual('object', obj.instance)
2700+
25142701
def test_decorated_async_methods_with_spec_mock(self):
25152702
class Foo():
25162703
@classmethod

Lib/unittest/mock.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ def _get_signature_object(func, as_instance, eat_self):
107107
eat_self = True
108108
# Use the original decorated method to extract the correct function signature
109109
func = func.__func__
110+
elif isinstance(func, partial):
111+
# inspect.signature() already accounts for a partial's bound
112+
# arguments; going through func.__call__ (a builtin method-wrapper)
113+
# would lose that and fail to produce a signature at all.
114+
pass
110115
elif not isinstance(func, FunctionTypes):
111116
# If we really want to model an instance of the passed type,
112117
# __call__ should be looked up, not __init__.
@@ -477,19 +482,29 @@ def __init__(
477482
__dict__['_mock_new_name'] = _new_name
478483
__dict__['_mock_new_parent'] = _new_parent
479484
__dict__['_mock_sealed'] = False
480-
__dict__['_autospec'] = autospec
485+
__dict__['_mock_autospec'] = autospec
481486

482487
if spec_set is not None:
483488
spec = spec_set
484489
spec_set = True
490+
491+
extra_spec_props = None
485492
if autospec is not None:
486493
# autospec is even stricter than spec_set.
494+
# an explicit spec given as a list of attribute names is preserved
495+
# as additional allowed attributes (e.g.: instance attributes set
496+
# in __init__, which won't show up via autospec).
497+
if spec is not None and _is_list(spec):
498+
extra_spec_props = spec
487499
spec = autospec
488500
autospec = True
501+
489502
if _eat_self is None:
490503
_eat_self = parent is not None
491504

492505
self._mock_add_spec(spec, spec_set, _spec_as_instance, _eat_self)
506+
if extra_spec_props is not None:
507+
self._mock_extend_spec_methods(extra_spec_props)
493508

494509
__dict__['_mock_children'] = {}
495510
__dict__['_mock_wraps'] = wraps
@@ -539,7 +554,7 @@ def mock_add_spec(self, spec, spec_set=False, autospec=None):
539554
checked.
540555
"""
541556
if autospec is not None:
542-
self.__dict__['_autospec'] = autospec
557+
self.__dict__['_mock_autospec'] = autospec
543558
self._mock_add_spec(spec, spec_set)
544559

545560

@@ -558,7 +573,7 @@ def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False,
558573
else:
559574
_spec_class = type(spec)
560575

561-
if self.__dict__.get('_autospec') is None:
576+
if self.__dict__.get('_mock_autospec') is None:
562577
res = _get_signature_object(spec, _spec_as_instance, _eat_self)
563578
else:
564579
res = _check_signature(spec, self, _eat_self,
@@ -732,7 +747,7 @@ def __getattr__(self, name):
732747
wraps = getattr(self._mock_wraps, name)
733748

734749
kwargs = {}
735-
if self.__dict__.get('_autospec') is not None:
750+
if self.__dict__.get('_mock_autospec') is not None:
736751
# get the mock's spec attribute with the same name and
737752
# pass it to the child.
738753
spec_class = self.__dict__.get('_spec_class')

0 commit comments

Comments
 (0)