Skip to content

Commit fdb9efc

Browse files
authored
bpo-41877: Check for misspelled speccing arguments (GH-23737)
patch, patch.object and create_autospec silently ignore misspelled arguments such as autospect, auto_spec and set_spec. This can lead to tests failing to check what they are supposed to check. This change adds a check causing a RuntimeError if the above functions get any of the above misspellings as arguments. It also adds a new argument, "unsafe", which can be set to True to disable this check. Also add "!r" to format specifiers in added error messages.
1 parent 42c9f0f commit fdb9efc

File tree

3 files changed

+84
-8
lines changed

3 files changed

+84
-8
lines changed

Lib/unittest/mock.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -633,8 +633,8 @@ def __getattr__(self, name):
633633
if not self._mock_unsafe:
634634
if name.startswith(('assert', 'assret', 'asert', 'aseert', 'assrt')):
635635
raise AttributeError(
636-
f"{name} is not a valid assertion. Use a spec "
637-
f"for the mock if {name} is meant to be an attribute.")
636+
f"{name!r} is not a valid assertion. Use a spec "
637+
f"for the mock if {name!r} is meant to be an attribute.")
638638

639639
result = self._mock_children.get(name)
640640
if result is _deleted:
@@ -1242,14 +1242,25 @@ def _importer(target):
12421242
return thing
12431243

12441244

1245+
# _check_spec_arg_typos takes kwargs from commands like patch and checks that
1246+
# they don't contain common misspellings of arguments related to autospeccing.
1247+
def _check_spec_arg_typos(kwargs_to_check):
1248+
typos = ("autospect", "auto_spec", "set_spec")
1249+
for typo in typos:
1250+
if typo in kwargs_to_check:
1251+
raise RuntimeError(
1252+
f"{typo!r} might be a typo; use unsafe=True if this is intended"
1253+
)
1254+
1255+
12451256
class _patch(object):
12461257

12471258
attribute_name = None
12481259
_active_patches = []
12491260

12501261
def __init__(
12511262
self, getter, attribute, new, spec, create,
1252-
spec_set, autospec, new_callable, kwargs
1263+
spec_set, autospec, new_callable, kwargs, *, unsafe=False
12531264
):
12541265
if new_callable is not None:
12551266
if new is not DEFAULT:
@@ -1260,6 +1271,8 @@ def __init__(
12601271
raise ValueError(
12611272
"Cannot use 'autospec' and 'new_callable' together"
12621273
)
1274+
if not unsafe:
1275+
_check_spec_arg_typos(kwargs)
12631276

12641277
self.getter = getter
12651278
self.attribute = attribute
@@ -1569,7 +1582,7 @@ def _get_target(target):
15691582
def _patch_object(
15701583
target, attribute, new=DEFAULT, spec=None,
15711584
create=False, spec_set=None, autospec=None,
1572-
new_callable=None, **kwargs
1585+
new_callable=None, *, unsafe=False, **kwargs
15731586
):
15741587
"""
15751588
patch the named member (`attribute`) on an object (`target`) with a mock
@@ -1591,7 +1604,7 @@ def _patch_object(
15911604
getter = lambda: target
15921605
return _patch(
15931606
getter, attribute, new, spec, create,
1594-
spec_set, autospec, new_callable, kwargs
1607+
spec_set, autospec, new_callable, kwargs, unsafe=unsafe
15951608
)
15961609

15971610

@@ -1646,7 +1659,7 @@ def _patch_multiple(target, spec=None, create=False, spec_set=None,
16461659

16471660
def patch(
16481661
target, new=DEFAULT, spec=None, create=False,
1649-
spec_set=None, autospec=None, new_callable=None, **kwargs
1662+
spec_set=None, autospec=None, new_callable=None, *, unsafe=False, **kwargs
16501663
):
16511664
"""
16521665
`patch` acts as a function decorator, class decorator or a context
@@ -1708,6 +1721,10 @@ def patch(
17081721
use "as" then the patched object will be bound to the name after the
17091722
"as"; very useful if `patch` is creating a mock object for you.
17101723
1724+
Patch will raise a `RuntimeError` if passed some common misspellings of
1725+
the arguments autospec and spec_set. Pass the argument `unsafe` with the
1726+
value True to disable that check.
1727+
17111728
`patch` takes arbitrary keyword arguments. These will be passed to
17121729
`AsyncMock` if the patched object is asynchronous, to `MagicMock`
17131730
otherwise or to `new_callable` if specified.
@@ -1718,7 +1735,7 @@ def patch(
17181735
getter, attribute = _get_target(target)
17191736
return _patch(
17201737
getter, attribute, new, spec, create,
1721-
spec_set, autospec, new_callable, kwargs
1738+
spec_set, autospec, new_callable, kwargs, unsafe=unsafe
17221739
)
17231740

17241741

@@ -2568,7 +2585,7 @@ def call_list(self):
25682585

25692586

25702587
def create_autospec(spec, spec_set=False, instance=False, _parent=None,
2571-
_name=None, **kwargs):
2588+
_name=None, *, unsafe=False, **kwargs):
25722589
"""Create a mock object using another object as a spec. Attributes on the
25732590
mock will use the corresponding attribute on the `spec` object as their
25742591
spec.
@@ -2584,6 +2601,10 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
25842601
spec for an instance object by passing `instance=True`. The returned mock
25852602
will only be callable if instances of the mock are callable.
25862603
2604+
`create_autospec` will raise a `RuntimeError` if passed some common
2605+
misspellings of the arguments autospec and spec_set. Pass the argument
2606+
`unsafe` with the value True to disable that check.
2607+
25872608
`create_autospec` also takes arbitrary keyword arguments that are passed to
25882609
the constructor of the created mock."""
25892610
if _is_list(spec):
@@ -2601,6 +2622,8 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
26012622
_kwargs = {}
26022623
if _kwargs and instance:
26032624
_kwargs['_spec_as_instance'] = True
2625+
if not unsafe:
2626+
_check_spec_arg_typos(kwargs)
26042627

26052628
_kwargs.update(kwargs)
26062629

Lib/unittest/test/testmock/testmock.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ def cmeth(cls, a, b, c, d=None): pass
3838
def smeth(a, b, c, d=None): pass
3939

4040

41+
class Typos():
42+
autospect = None
43+
auto_spec = None
44+
set_spec = None
45+
46+
4147
def something(a): pass
4248

4349

@@ -2175,6 +2181,52 @@ def __init__(self):
21752181

21762182
self.assertEqual(obj.obj_with_bool_func.__bool__.call_count, 0)
21772183

2184+
def test_misspelled_arguments(self):
2185+
class Foo():
2186+
one = 'one'
2187+
# patch, patch.object and create_autospec need to check for misspelled
2188+
# arguments explicitly and throw a RuntimError if found.
2189+
with self.assertRaises(RuntimeError):
2190+
with patch(f'{__name__}.Something.meth', autospect=True): pass
2191+
with self.assertRaises(RuntimeError):
2192+
with patch.object(Foo, 'one', autospect=True): pass
2193+
with self.assertRaises(RuntimeError):
2194+
with patch(f'{__name__}.Something.meth', auto_spec=True): pass
2195+
with self.assertRaises(RuntimeError):
2196+
with patch.object(Foo, 'one', auto_spec=True): pass
2197+
with self.assertRaises(RuntimeError):
2198+
with patch(f'{__name__}.Something.meth', set_spec=True): pass
2199+
with self.assertRaises(RuntimeError):
2200+
with patch.object(Foo, 'one', set_spec=True): pass
2201+
with self.assertRaises(RuntimeError):
2202+
m = create_autospec(Foo, set_spec=True)
2203+
# patch.multiple, on the other hand, should flag misspelled arguments
2204+
# through an AttributeError, when trying to find the keys from kwargs
2205+
# as attributes on the target.
2206+
with self.assertRaises(AttributeError):
2207+
with patch.multiple(
2208+
f'{__name__}.Something', meth=DEFAULT, autospect=True): pass
2209+
with self.assertRaises(AttributeError):
2210+
with patch.multiple(
2211+
f'{__name__}.Something', meth=DEFAULT, auto_spec=True): pass
2212+
with self.assertRaises(AttributeError):
2213+
with patch.multiple(
2214+
f'{__name__}.Something', meth=DEFAULT, set_spec=True): pass
2215+
2216+
with patch(f'{__name__}.Something.meth', unsafe=True, autospect=True):
2217+
pass
2218+
with patch.object(Foo, 'one', unsafe=True, autospect=True): pass
2219+
with patch(f'{__name__}.Something.meth', unsafe=True, auto_spec=True):
2220+
pass
2221+
with patch.object(Foo, 'one', unsafe=True, auto_spec=True): pass
2222+
with patch(f'{__name__}.Something.meth', unsafe=True, set_spec=True):
2223+
pass
2224+
with patch.object(Foo, 'one', unsafe=True, set_spec=True): pass
2225+
m = create_autospec(Foo, set_spec=True, unsafe=True)
2226+
with patch.multiple(
2227+
f'{__name__}.Typos', autospect=True, set_spec=True, auto_spec=True):
2228+
pass
2229+
21782230

21792231
if __name__ == '__main__':
21802232
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A check is added against misspellings of autospect, auto_spec and set_spec being passed as arguments to patch, patch.object and create_autospec.

0 commit comments

Comments
 (0)