mirror of
https://github.com/python/cpython.git
synced 2025-08-04 00:48:58 +00:00
bpo-33453: Handle string type annotations in dataclasses. (GH-6768)
This commit is contained in:
parent
d8dcd57edb
commit
2a7bacbd91
7 changed files with 399 additions and 20 deletions
|
@ -12,6 +12,9 @@ from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Op
|
|||
from collections import deque, OrderedDict, namedtuple
|
||||
from functools import total_ordering
|
||||
|
||||
import typing # Needed for the string "typing.ClassVar[int]" to work as an annotation.
|
||||
import dataclasses # Needed for the string "dataclasses.InitVar[int]" to work as an annotation.
|
||||
|
||||
# Just any custom exception we can catch.
|
||||
class CustomError(Exception): pass
|
||||
|
||||
|
@ -600,7 +603,6 @@ class TestCase(unittest.TestCase):
|
|||
class C:
|
||||
x: ClassVar[typ] = Subclass()
|
||||
|
||||
|
||||
def test_deliberately_mutable_defaults(self):
|
||||
# If a mutable default isn't in the known list of
|
||||
# (list, dict, set), then it's okay.
|
||||
|
@ -924,14 +926,16 @@ class TestCase(unittest.TestCase):
|
|||
z: ClassVar[int] = 1000
|
||||
w: ClassVar[int] = 2000
|
||||
t: ClassVar[int] = 3000
|
||||
s: ClassVar = 4000
|
||||
|
||||
c = C(5)
|
||||
self.assertEqual(repr(c), 'TestCase.test_class_var.<locals>.C(x=5, y=10)')
|
||||
self.assertEqual(len(fields(C)), 2) # We have 2 fields.
|
||||
self.assertEqual(len(C.__annotations__), 5) # And 3 ClassVars.
|
||||
self.assertEqual(len(C.__annotations__), 6) # And 4 ClassVars.
|
||||
self.assertEqual(c.z, 1000)
|
||||
self.assertEqual(c.w, 2000)
|
||||
self.assertEqual(c.t, 3000)
|
||||
self.assertEqual(c.s, 4000)
|
||||
C.z += 1
|
||||
self.assertEqual(c.z, 1001)
|
||||
c = C(20)
|
||||
|
@ -939,6 +943,7 @@ class TestCase(unittest.TestCase):
|
|||
self.assertEqual(c.z, 1001)
|
||||
self.assertEqual(c.w, 2000)
|
||||
self.assertEqual(c.t, 3000)
|
||||
self.assertEqual(c.s, 4000)
|
||||
|
||||
def test_class_var_no_default(self):
|
||||
# If a ClassVar has no default value, it should not be set on the class.
|
||||
|
@ -2798,5 +2803,149 @@ class TestDescriptors(unittest.TestCase):
|
|||
self.assertEqual(D.__set_name__.call_count, 1)
|
||||
|
||||
|
||||
class TestStringAnnotations(unittest.TestCase):
|
||||
def test_classvar(self):
|
||||
# Some expressions recognized as ClassVar really aren't. But
|
||||
# if you're using string annotations, it's not an exact
|
||||
# science.
|
||||
# These tests assume that both "import typing" and "from
|
||||
# typing import *" have been run in this file.
|
||||
for typestr in ('ClassVar[int]',
|
||||
'ClassVar [int]'
|
||||
' ClassVar [int]',
|
||||
'ClassVar',
|
||||
' ClassVar ',
|
||||
'typing.ClassVar[int]',
|
||||
'typing.ClassVar[str]',
|
||||
' typing.ClassVar[str]',
|
||||
'typing .ClassVar[str]',
|
||||
'typing. ClassVar[str]',
|
||||
'typing.ClassVar [str]',
|
||||
'typing.ClassVar [ str]',
|
||||
|
||||
# Not syntactically valid, but these will
|
||||
# be treated as ClassVars.
|
||||
'typing.ClassVar.[int]',
|
||||
'typing.ClassVar+',
|
||||
):
|
||||
with self.subTest(typestr=typestr):
|
||||
@dataclass
|
||||
class C:
|
||||
x: typestr
|
||||
|
||||
# x is a ClassVar, so C() takes no args.
|
||||
C()
|
||||
|
||||
# And it won't appear in the class's dict because it doesn't
|
||||
# have a default.
|
||||
self.assertNotIn('x', C.__dict__)
|
||||
|
||||
def test_isnt_classvar(self):
|
||||
for typestr in ('CV',
|
||||
't.ClassVar',
|
||||
't.ClassVar[int]',
|
||||
'typing..ClassVar[int]',
|
||||
'Classvar',
|
||||
'Classvar[int]',
|
||||
'typing.ClassVarx[int]',
|
||||
'typong.ClassVar[int]',
|
||||
'dataclasses.ClassVar[int]',
|
||||
'typingxClassVar[str]',
|
||||
):
|
||||
with self.subTest(typestr=typestr):
|
||||
@dataclass
|
||||
class C:
|
||||
x: typestr
|
||||
|
||||
# x is not a ClassVar, so C() takes one arg.
|
||||
self.assertEqual(C(10).x, 10)
|
||||
|
||||
def test_initvar(self):
|
||||
# These tests assume that both "import dataclasses" and "from
|
||||
# dataclasses import *" have been run in this file.
|
||||
for typestr in ('InitVar[int]',
|
||||
'InitVar [int]'
|
||||
' InitVar [int]',
|
||||
'InitVar',
|
||||
' InitVar ',
|
||||
'dataclasses.InitVar[int]',
|
||||
'dataclasses.InitVar[str]',
|
||||
' dataclasses.InitVar[str]',
|
||||
'dataclasses .InitVar[str]',
|
||||
'dataclasses. InitVar[str]',
|
||||
'dataclasses.InitVar [str]',
|
||||
'dataclasses.InitVar [ str]',
|
||||
|
||||
# Not syntactically valid, but these will
|
||||
# be treated as InitVars.
|
||||
'dataclasses.InitVar.[int]',
|
||||
'dataclasses.InitVar+',
|
||||
):
|
||||
with self.subTest(typestr=typestr):
|
||||
@dataclass
|
||||
class C:
|
||||
x: typestr
|
||||
|
||||
# x is an InitVar, so doesn't create a member.
|
||||
with self.assertRaisesRegex(AttributeError,
|
||||
"object has no attribute 'x'"):
|
||||
C(1).x
|
||||
|
||||
def test_isnt_initvar(self):
|
||||
for typestr in ('IV',
|
||||
'dc.InitVar',
|
||||
'xdataclasses.xInitVar',
|
||||
'typing.xInitVar[int]',
|
||||
):
|
||||
with self.subTest(typestr=typestr):
|
||||
@dataclass
|
||||
class C:
|
||||
x: typestr
|
||||
|
||||
# x is not an InitVar, so there will be a member x.
|
||||
self.assertEqual(C(10).x, 10)
|
||||
|
||||
def test_classvar_module_level_import(self):
|
||||
from . import dataclass_module_1
|
||||
from . import dataclass_module_1_str
|
||||
from . import dataclass_module_2
|
||||
from . import dataclass_module_2_str
|
||||
|
||||
for m in (dataclass_module_1, dataclass_module_1_str,
|
||||
dataclass_module_2, dataclass_module_2_str,
|
||||
):
|
||||
with self.subTest(m=m):
|
||||
# There's a difference in how the ClassVars are
|
||||
# interpreted when using string annotations or
|
||||
# not. See the imported modules for details.
|
||||
if m.USING_STRINGS:
|
||||
c = m.CV(10)
|
||||
else:
|
||||
c = m.CV()
|
||||
self.assertEqual(c.cv0, 20)
|
||||
|
||||
|
||||
# There's a difference in how the InitVars are
|
||||
# interpreted when using string annotations or
|
||||
# not. See the imported modules for details.
|
||||
c = m.IV(0, 1, 2, 3, 4)
|
||||
|
||||
for field_name in ('iv0', 'iv1', 'iv2', 'iv3'):
|
||||
with self.subTest(field_name=field_name):
|
||||
with self.assertRaisesRegex(AttributeError, f"object has no attribute '{field_name}'"):
|
||||
# Since field_name is an InitVar, it's
|
||||
# not an instance field.
|
||||
getattr(c, field_name)
|
||||
|
||||
if m.USING_STRINGS:
|
||||
# iv4 is interpreted as a normal field.
|
||||
self.assertIn('not_iv4', c.__dict__)
|
||||
self.assertEqual(c.not_iv4, 4)
|
||||
else:
|
||||
# iv4 is interpreted as an InitVar, so it
|
||||
# won't exist on the instance.
|
||||
self.assertNotIn('not_iv4', c.__dict__)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue