Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-82129: Provide __annotate__ method for dataclasses from make_dataclass #122262

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
23 changes: 21 additions & 2 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1527,10 +1527,11 @@ class C(Base):
seen = set()
annotations = {}
defaults = {}
any_marker = object()
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 @@ -1549,11 +1550,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, _convert_to_source
ann_dict = {
ann: Any if t is any_marker else t
for ann, t in annotations.items()
}
if format == annotationlib.Format.SOURCE:
return _convert_to_source(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
47 changes: 41 additions & 6 deletions Lib/test/test_dataclasses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from dataclasses import *

import abc
import annotationlib
import io
import pickle
import inspect
Expand All @@ -23,6 +24,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 @@ -3667,7 +3669,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 @@ -4159,16 +4160,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.SOURCE),
{'x': 'typing.Any', 'y': 'int', 'z': 'typing.Any'},
)

def test_no_types_no_typing_import(self):
import sys

C = make_dataclass('C', ['x', ('y', int)])

with import_helper.CleanImport('typing'):
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