Skip to content

Commit 99948cf

Browse files
committed
pythongh-124176: Add special support for dataclasses to create_autospec
1 parent e670a11 commit 99948cf

File tree

3 files changed

+92
-6
lines changed

3 files changed

+92
-6
lines changed

Lib/test/test_unittest/testmock/testhelpers.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,75 @@ def f(a): pass
10341034
self.assertEqual(mock.mock_calls, [])
10351035
self.assertEqual(rv.mock_calls, [])
10361036

1037+
def test_dataclass(self):
1038+
from dataclasses import dataclass, field, InitVar
1039+
from typing import ClassVar
1040+
1041+
@dataclass
1042+
class WithPostInit:
1043+
a: int = field(init=False)
1044+
b: int = field(init=False)
1045+
def __post_init__(self):
1046+
self.a = 1
1047+
self.b = 2
1048+
1049+
for mock in [
1050+
create_autospec(WithPostInit, instance=True),
1051+
create_autospec(WithPostInit()),
1052+
]:
1053+
with self.subTest(mock=mock):
1054+
self.assertIsInstance(mock.a, int)
1055+
self.assertIsInstance(mock.b, int)
1056+
1057+
# Classes do not have these fields:
1058+
mock = create_autospec(WithPostInit)
1059+
msg = "Mock object has no attribute"
1060+
with self.assertRaisesRegex(AttributeError, msg):
1061+
mock.a
1062+
with self.assertRaisesRegex(AttributeError, msg):
1063+
mock.b
1064+
1065+
@dataclass
1066+
class WithDefault:
1067+
a: int
1068+
b: int = 0
1069+
1070+
for mock in [
1071+
create_autospec(WithDefault, instance=True),
1072+
create_autospec(WithDefault(1)),
1073+
]:
1074+
with self.subTest(mock=mock):
1075+
self.assertIsInstance(mock.a, int)
1076+
self.assertIsInstance(mock.b, int)
1077+
1078+
@dataclass
1079+
class WithMethod:
1080+
a: int
1081+
def b(self) -> int:
1082+
return 1
1083+
1084+
for mock in [
1085+
create_autospec(WithMethod, instance=True),
1086+
create_autospec(WithMethod(1)),
1087+
]:
1088+
with self.subTest(mock=mock):
1089+
self.assertIsInstance(mock.a, int)
1090+
mock.b.assert_not_called()
1091+
1092+
@dataclass
1093+
class WithNonFields:
1094+
a: ClassVar[int]
1095+
b: InitVar[int]
1096+
1097+
for mock in [
1098+
create_autospec(WithNonFields, instance=True),
1099+
create_autospec(WithNonFields(1)),
1100+
]:
1101+
with self.subTest(mock=mock):
1102+
with self.assertRaisesRegex(AttributeError, msg):
1103+
mock.a
1104+
with self.assertRaisesRegex(AttributeError, msg):
1105+
mock.b
10371106

10381107
class TestCallList(unittest.TestCase):
10391108

Lib/unittest/mock.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2754,7 +2754,19 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
27542754
raise InvalidSpecError(f'Cannot autospec a Mock object. '
27552755
f'[object={spec!r}]')
27562756
is_async_func = _is_async_func(spec)
2757-
_kwargs = {'spec': spec}
2757+
2758+
placeholder = object()
2759+
entries = [(entry, placeholder) for entry in dir(spec)]
2760+
# Not using `is_dataclass` to avoid an import of dataclasses module
2761+
# for types that don't need that.
2762+
if is_type and instance and hasattr(spec, '__dataclass_fields__'):
2763+
from dataclasses import fields
2764+
dataclass_fields = fields(spec)
2765+
entries.extend((f.name, f.type) for f in dataclass_fields)
2766+
_kwargs = {'spec': [f.name for f in dataclass_fields]}
2767+
else:
2768+
_kwargs = {'spec': spec}
2769+
27582770
if spec_set:
27592771
_kwargs = {'spec_set': spec}
27602772
elif spec is None:
@@ -2811,7 +2823,7 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
28112823
_name='()', _parent=mock,
28122824
wraps=wrapped)
28132825

2814-
for entry in dir(spec):
2826+
for entry, original in entries:
28152827
if _is_magic(entry):
28162828
# MagicMock already does the useful magic methods for us
28172829
continue
@@ -2825,10 +2837,11 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
28252837
# AttributeError on being fetched?
28262838
# we could be resilient against it, or catch and propagate the
28272839
# exception when the attribute is fetched from the mock
2828-
try:
2829-
original = getattr(spec, entry)
2830-
except AttributeError:
2831-
continue
2840+
if original is placeholder:
2841+
try:
2842+
original = getattr(spec, entry)
2843+
except AttributeError:
2844+
continue
28322845

28332846
child_kwargs = {'spec': original}
28342847
# Wrap child attributes also.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add support for :func:`dataclasses.dataclass` in
2+
:func:`unittest.mock.create_autospec`. Now ``create_autospec`` will check
3+
for potential dataclasses and use :func:`dataclasses.fields` function to
4+
retrieve the spec information.

0 commit comments

Comments
 (0)