diff options
author | Eric V. Smith <ericvsmith@users.noreply.github.com> | 2018-05-16 02:44:27 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-05-16 02:44:27 (GMT) |
commit | 2a7bacbd913cf2bf568b3c0f85a758946d3cf4e9 (patch) | |
tree | 72139540fcd8f6824c8265cb7ed4515b3d3a0c37 /Lib/test/test_dataclasses.py | |
parent | d8dcd57edb88ce57063e5c2b85fe0ee1abb1ce8b (diff) | |
download | cpython-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-x | Lib/test/test_dataclasses.py | 153 |
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() |