Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions Lib/dataclasses.py
Original file line numberDiff line numberDiff 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 DownExpand 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
MemberAuthor

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
MemberAuthor

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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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