summaryrefslogtreecommitdiffstats
path: root/Lib/test/test_dataclasses.py
diff options
context:
space:
mode:
authorEric V. Smith <ericvsmith@users.noreply.github.com>2018-05-16 02:44:27 (GMT)
committerGitHub <noreply@github.com>2018-05-16 02:44:27 (GMT)
commit2a7bacbd913cf2bf568b3c0f85a758946d3cf4e9 (patch)
tree72139540fcd8f6824c8265cb7ed4515b3d3a0c37 /Lib/test/test_dataclasses.py
parentd8dcd57edb88ce57063e5c2b85fe0ee1abb1ce8b (diff)
downloadcpython-2a7bacbd913cf2bf568b3c0f85a758946d3cf4e9.zip
cpython-2a7bacbd913cf2bf568b3c0f85a758946d3cf4e9.tar.gz
cpython-2a7bacbd913cf2bf568b3c0f85a758946d3cf4e9.tar.bz2
bpo-33453: Handle string type annotations in dataclasses. (GH-6768)
Diffstat (limited to 'Lib/test/test_dataclasses.py')
-rwxr-xr-xLib/test/test_dataclasses.py153
1 files changed, 151 insertions, 2 deletions
diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py
index 2c890a2..b251c04 100755
--- a/Lib/test/test_dataclasses.py
+++ b/Lib/test/test_dataclasses.py
@@ -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()