diff --git a/Lib/test/test_unittest/testmock/testhelpers.py b/Lib/test/test_unittest/testmock/testhelpers.py index f260769eb8c35e..b1d59047c9d472 100644 --- a/Lib/test/test_unittest/testmock/testhelpers.py +++ b/Lib/test/test_unittest/testmock/testhelpers.py @@ -1107,6 +1107,32 @@ class WithNonFields: with self.assertRaisesRegex(AttributeError, msg): mock.b + def test_dataclass_default_value_type_overrides_field_annotation(self): + # If field defines an actual default, we don't need to change + # the default type. Since this is how it used to work before #124176 + @dataclass + class WithUnionAnnotation: + narrow_default: int | None = field(default=30) + + for mock in [ + create_autospec(WithUnionAnnotation, instance=True), + create_autospec(WithUnionAnnotation()), + ]: + with self.subTest(mock=mock): + self.assertIs(mock.narrow_default.__class__, int) + + def test_dataclass_field_with_no_default_value(self): + @dataclass + class WithUnionAnnotation: + no_default: int | None + + mock = create_autospec(WithUnionAnnotation, instance=True) + self.assertIs(mock.no_default.__class__, type(int | None)) + + mock = create_autospec(WithUnionAnnotation(1)) + self.assertIs(mock.no_default.__class__, int) + + class TestCallList(unittest.TestCase): def test_args_list_contains_call_list(self): diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 21ca061a77c26f..2375c5f0467034 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2758,13 +2758,19 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, f'[object={spec!r}]') is_async_func = _is_async_func(spec) - entries = [(entry, _missing) for entry in dir(spec)] + base_entries = {entry: _missing for entry in dir(spec)} if is_type and instance and is_dataclass(spec): + # Dataclass instance mocks created from a class may not have all of their fields + # prepopulated with default values. Create an initial set of attribute entries from + # the dataclass field annotations, but override them with the actual attribute types + # when fields have already been populated. dataclass_fields = fields(spec) - entries.extend((f.name, f.type) for f in dataclass_fields) + entries = {f.name: f.type for f in dataclass_fields} + entries.update(base_entries) _kwargs = {'spec': [f.name for f in dataclass_fields]} else: _kwargs = {'spec': spec} + entries = base_entries if spec_set: _kwargs = {'spec_set': spec} @@ -2822,7 +2828,7 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, _name='()', _parent=mock, wraps=wrapped) - for entry, original in entries: + for entry, original in entries.items(): if _is_magic(entry): # MagicMock already does the useful magic methods for us continue