Skip to content
26 changes: 24 additions & 2 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ def __repr__(self):
property,
})

# Any marker is used in `make_dataclass` to mark unannotated fields as `Any`
# without importing `typing` module.
_ANY_MARKER = object()


class InitVar:
__slots__ = ('type', )
Expand Down Expand Up @@ -1591,7 +1595,7 @@ class C(Base):
for item in fields:
if isinstance(item, str):
name = item
tp = 'typing.Any'
tp = _ANY_MARKER
elif len(item) == 2:
name, tp, = item
elif len(item) == 3:
Expand All @@ -1610,11 +1614,29 @@ class C(Base):
seen.add(name)
annotations[name] = tp

def annotate_method(format):
typing = sys.modules.get("typing")
if typing is None and format == annotationlib.Format.FORWARDREF:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also avoid importing typing for the SOURCE format here I think; is that worth it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure we can. I need _convert_to_source, there can be complex annotations that should be formatted properly. I will open a new issue about converting annotations to string with public API though. Right now I don't see a clear way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened #124412 for that.

typing_any = annotationlib.ForwardRef("Any", module="typing")
return {
ann: typing_any if t is _ANY_MARKER else t
for ann, t in annotations.items()
}

from typing import Any
ann_dict = {
ann: Any if t is _ANY_MARKER else t
for ann, t in annotations.items()
}
if format == annotationlib.Format.STRING:
return annotationlib.annotations_to_string(ann_dict)
return ann_dict

# Update 'ns' with the user-supplied namespace plus our calculated values.
def exec_body_callback(ns):
ns['__annotate__'] = annotate_method
ns.update(namespace)
ns.update(defaults)
ns['__annotations__'] = annotations

# We use `types.new_class()` instead of simply `type()` to allow dynamic creation
# of generic dataclasses.
Expand Down
48 changes: 42 additions & 6 deletions Lib/test/test_dataclasses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
from dataclasses import *

import abc
import annotationlib
import io
import pickle
import inspect
import builtins
import types
import weakref
import traceback
import sys
import textwrap
import unittest
from unittest.mock import Mock
Expand All @@ -25,6 +27,7 @@
import dataclasses # Needed for the string "dataclasses.InitVar[int]" to work as an annotation.

from test import support
from test.support import import_helper

# Just any custom exception we can catch.
class CustomError(Exception): pass
Expand Down Expand Up @@ -3754,7 +3757,6 @@ class A(WithDictSlot): ...
@support.cpython_only
def test_dataclass_slot_dict_ctype(self):
# https://github.com/python/cpython/issues/123935
from test.support import import_helper
# Skips test if `_testcapi` is not present:
_testcapi = import_helper.import_module('_testcapi')

Expand Down Expand Up @@ -4246,16 +4248,50 @@ def test_no_types(self):
C = make_dataclass('Point', ['x', 'y', 'z'])
c = C(1, 2, 3)
self.assertEqual(vars(c), {'x': 1, 'y': 2, 'z': 3})
self.assertEqual(C.__annotations__, {'x': 'typing.Any',
'y': 'typing.Any',
'z': 'typing.Any'})
self.assertEqual(C.__annotations__, {'x': typing.Any,
'y': typing.Any,
'z': typing.Any})

C = make_dataclass('Point', ['x', ('y', int), 'z'])
c = C(1, 2, 3)
self.assertEqual(vars(c), {'x': 1, 'y': 2, 'z': 3})
self.assertEqual(C.__annotations__, {'x': 'typing.Any',
self.assertEqual(C.__annotations__, {'x': typing.Any,
'y': int,
'z': 'typing.Any'})
'z': typing.Any})

def test_no_types_get_annotations(self):
C = make_dataclass('C', ['x', ('y', int), 'z'])

self.assertEqual(
annotationlib.get_annotations(C, format=annotationlib.Format.VALUE),
{'x': typing.Any, 'y': int, 'z': typing.Any},
)
self.assertEqual(
annotationlib.get_annotations(
C, format=annotationlib.Format.FORWARDREF),
{'x': typing.Any, 'y': int, 'z': typing.Any},
)
self.assertEqual(
annotationlib.get_annotations(
C, format=annotationlib.Format.STRING),
{'x': 'typing.Any', 'y': 'int', 'z': 'typing.Any'},
)

def test_no_types_no_typing_import(self):
with import_helper.CleanImport('typing'):
self.assertNotIn('typing', sys.modules)
C = make_dataclass('C', ['x', ('y', int)])

self.assertNotIn('typing', sys.modules)
self.assertEqual(
annotationlib.get_annotations(
C, format=annotationlib.Format.FORWARDREF),
{
'x': annotationlib.ForwardRef('Any', module='typing'),
'y': int,
},
)
self.assertNotIn('typing', sys.modules)

def test_module_attr(self):
self.assertEqual(ByMakeDataClass.__module__, __name__)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :exc:`NameError` when calling :func:`typing.get_type_hints` on a :func:`dataclasses.dataclass` created by
:func:`dataclasses.make_dataclass` with un-annotated fields.
Loading