Skip to content

Commit 7a018f1

Browse files
sobolevncjw296
authored andcommitted
gh-124176: Add special support for dataclasses to create_autospec (#124429)
Backports: 3a0e7f57628466aedcaaf6c5ff7c8224f5155a2c Signed-off-by: Chris Withers <[email protected]>
1 parent 6b8770e commit 7a018f1

File tree

3 files changed

+107
-6
lines changed

3 files changed

+107
-6
lines changed
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.

mock/mock.py

+22-6
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@
3232
import sys
3333
import threading
3434
import builtins
35+
36+
try:
37+
from dataclasses import fields, is_dataclass
38+
HAS_DATACLASSES = True
39+
except ImportError:
40+
HAS_DATACLASSES = False
41+
3542
from types import CodeType, ModuleType, MethodType
3643
from unittest.util import safe_repr
3744
from functools import wraps, partial
@@ -2808,7 +2815,15 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
28082815
raise InvalidSpecError(f'Cannot autospec a Mock object. '
28092816
f'[object={spec!r}]')
28102817
is_async_func = _is_async_func(spec)
2811-
_kwargs = {'spec': spec}
2818+
2819+
entries = [(entry, _missing) for entry in dir(spec)]
2820+
if is_type and instance and HAS_DATACLASSES and is_dataclass(spec):
2821+
dataclass_fields = fields(spec)
2822+
entries.extend((f.name, f.type) for f in dataclass_fields)
2823+
_kwargs = {'spec': [f.name for f in dataclass_fields]}
2824+
else:
2825+
_kwargs = {'spec': spec}
2826+
28122827
if spec_set:
28132828
_kwargs = {'spec_set': spec}
28142829
elif spec is None:
@@ -2865,7 +2880,7 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
28652880
_name='()', _parent=mock,
28662881
wraps=wrapped)
28672882

2868-
for entry in dir(spec):
2883+
for entry, original in entries:
28692884
if _is_magic(entry):
28702885
# MagicMock already does the useful magic methods for us
28712886
continue
@@ -2879,10 +2894,11 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
28792894
# AttributeError on being fetched?
28802895
# we could be resilient against it, or catch and propagate the
28812896
# exception when the attribute is fetched from the mock
2882-
try:
2883-
original = getattr(spec, entry)
2884-
except AttributeError:
2885-
continue
2897+
if original is _missing:
2898+
try:
2899+
original = getattr(spec, entry)
2900+
except AttributeError:
2901+
continue
28862902

28872903
child_kwargs = {'spec': original}
28882904
# Wrap child attributes also.

mock/tests/testhelpers.py

+81
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,17 @@
1010
from mock.mock import _Call, _CallList, _callable
1111
from mock import IS_PYPY
1212

13+
try:
14+
from dataclasses import dataclass, field, InitVar
15+
except ImportError:
16+
pass
17+
1318
from datetime import datetime
1419
from functools import partial
20+
from typing import ClassVar
1521

1622
import pytest
23+
import sys
1724

1825

1926
class SomeClass(object):
@@ -1043,6 +1050,80 @@ def f(a): pass
10431050
self.assertEqual(mock.mock_calls, [])
10441051
self.assertEqual(rv.mock_calls, [])
10451052

1053+
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python 3.7 or higher")
1054+
def test_dataclass_post_init(self):
1055+
@dataclass
1056+
class WithPostInit:
1057+
a: int = field(init=False)
1058+
b: int = field(init=False)
1059+
def __post_init__(self):
1060+
self.a = 1
1061+
self.b = 2
1062+
1063+
for mock in [
1064+
create_autospec(WithPostInit, instance=True),
1065+
create_autospec(WithPostInit()),
1066+
]:
1067+
with self.subTest(mock=mock):
1068+
self.assertIsInstance(mock.a, int)
1069+
self.assertIsInstance(mock.b, int)
1070+
1071+
# Classes do not have these fields:
1072+
mock = create_autospec(WithPostInit)
1073+
msg = "Mock object has no attribute"
1074+
with self.assertRaisesRegex(AttributeError, msg):
1075+
mock.a
1076+
with self.assertRaisesRegex(AttributeError, msg):
1077+
mock.b
1078+
1079+
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python 3.7 or higher")
1080+
def test_dataclass_default(self):
1081+
@dataclass
1082+
class WithDefault:
1083+
a: int
1084+
b: int = 0
1085+
1086+
for mock in [
1087+
create_autospec(WithDefault, instance=True),
1088+
create_autospec(WithDefault(1)),
1089+
]:
1090+
with self.subTest(mock=mock):
1091+
self.assertIsInstance(mock.a, int)
1092+
self.assertIsInstance(mock.b, int)
1093+
1094+
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python 3.7 or higher")
1095+
def test_dataclass_with_method(self):
1096+
@dataclass
1097+
class WithMethod:
1098+
a: int
1099+
def b(self) -> int:
1100+
return 1
1101+
1102+
for mock in [
1103+
create_autospec(WithMethod, instance=True),
1104+
create_autospec(WithMethod(1)),
1105+
]:
1106+
with self.subTest(mock=mock):
1107+
self.assertIsInstance(mock.a, int)
1108+
mock.b.assert_not_called()
1109+
1110+
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python 3.7 or higher")
1111+
def test_dataclass_with_non_fields(self):
1112+
@dataclass
1113+
class WithNonFields:
1114+
a: ClassVar[int]
1115+
b: InitVar[int]
1116+
1117+
msg = "Mock object has no attribute"
1118+
for mock in [
1119+
create_autospec(WithNonFields, instance=True),
1120+
create_autospec(WithNonFields(1)),
1121+
]:
1122+
with self.subTest(mock=mock):
1123+
with self.assertRaisesRegex(AttributeError, msg):
1124+
mock.a
1125+
with self.assertRaisesRegex(AttributeError, msg):
1126+
mock.b
10461127

10471128
class TestCallList(unittest.TestCase):
10481129

0 commit comments

Comments
 (0)