diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index e766a7b554afe1..016660ff478917 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -220,7 +220,8 @@ def __repr__(self): # String regex that string annotations for ClassVar or InitVar must match. # Allows "identifier.identifier[" or "identifier[". # https://bugs.python.org/issue33453 for details. -_MODULE_IDENTIFIER_RE = re.compile(r'^(?:\s*(\w+)\s*\.)?\s*(\w+)') +_MODULE_IDENTIFIER_RE = re.compile( + r'^(?:(?:\w+\s*\.\s*)?Annotated\s*\[\s*)*(?:(\w+)\s*\.)?\s*(\w+)') # Atomic immutable types which don't require any recursive handling and for which deepcopy # returns the same object. We can provide a fast-path for these types in asdict and astuple. @@ -774,6 +775,13 @@ def _get_field(cls, a_name, a_type, default_kw_only): default = MISSING f = field(default=default) + typing = sys.modules.get('typing') + if typing: + while isinstance(a_type, typing._AnnotatedAlias): + a_type = a_type.__origin__ + if isinstance(a_type, typing.ForwardRef): + a_type = a_type.__forward_arg__ + # Only at this point do we know the name and the type. Set them. f.name = a_name f.type = a_type @@ -797,7 +805,7 @@ def _get_field(cls, a_name, a_type, default_kw_only): # annotation to be a ClassVar. So, only look for ClassVar if # typing has been imported by any module (not necessarily cls's # module). - typing = sys.modules.get('typing') + if typing: if (_is_classvar(a_type, typing) or (isinstance(f.type, str) diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index 6669f1c57e2e78..13220188df5eb7 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -14,7 +14,7 @@ import traceback import unittest from unittest.mock import Mock -from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional, Protocol, DefaultDict +from typing import Annotated, ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional, Protocol, DefaultDict from typing import get_type_hints from collections import deque, OrderedDict, namedtuple, defaultdict from functools import total_ordering @@ -1153,19 +1153,29 @@ def test_class_var(self): class C: x: int y: int = 10 - z: ClassVar[int] = 1000 - w: ClassVar[int] = 2000 - t: ClassVar[int] = 3000 - s: ClassVar = 4000 + z: ClassVar[int] = 1000 + w: ClassVar[int] = 2000 + t: ClassVar[int] = 3000 + s: ClassVar = 4000 + a: Annotated[ClassVar, 'meta'] = 5000 + b: Annotated[ClassVar[int], 'meta'] = 6000 + c: Annotated['ClassVar', 'meta'] = 7000 + d: Annotated[Annotated[ClassVar, 'meta'], 'meta'] = 8000 + e: Annotated['Annotated[ClassVar, "meta"]', 'meta'] = 9000 c = C(5) self.assertEqual(repr(c), 'TestCase.test_class_var..C(x=5, y=10)') self.assertEqual(len(fields(C)), 2) # We have 2 fields. - self.assertEqual(len(C.__annotations__), 6) # And 4 ClassVars. + self.assertEqual(len(C.__annotations__), 11) # And 9 ClassVars. self.assertEqual(c.z, 1000) self.assertEqual(c.w, 2000) self.assertEqual(c.t, 3000) self.assertEqual(c.s, 4000) + self.assertEqual(c.a, 5000) + self.assertEqual(c.b, 6000) + self.assertEqual(c.c, 7000) + self.assertEqual(c.d, 8000) + self.assertEqual(c.e, 9000) C.z += 1 self.assertEqual(c.z, 1001) c = C(20) @@ -1174,6 +1184,11 @@ class C: self.assertEqual(c.w, 2000) self.assertEqual(c.t, 3000) self.assertEqual(c.s, 4000) + self.assertEqual(c.a, 5000) + self.assertEqual(c.b, 6000) + self.assertEqual(c.c, 7000) + self.assertEqual(c.d, 8000) + self.assertEqual(c.e, 9000) def test_class_var_no_default(self): # If a ClassVar has no default value, it should not be set on the class. @@ -1269,13 +1284,14 @@ def test_init_var(self): class C: x: int = None init_param: InitVar[int] = None + annotated_init_param: Annotated[InitVar[int], 'meta'] = None - def __post_init__(self, init_param): + def __post_init__(self, init_param, annotated_init_param): if self.x is None: - self.x = init_param*2 + self.x = init_param*2 + annotated_init_param - c = C(init_param=10) - self.assertEqual(c.x, 20) + c = C(init_param=10, annotated_init_param=5) + self.assertEqual(c.x, 25) def test_init_var_preserve_type(self): self.assertEqual(InitVar[int].type, int) @@ -3590,16 +3606,21 @@ def test_classvar(self): # 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]', + 'Annotated[ClassVar[int], (3, 5)]', + 'Annotated[Annotated[ClassVar[int], (3, 5)], (3, 6)]', + 'Annotated[typing.ClassVar[int], (3, 5)]', + 'Annotated [ClassVar[int], (3, 5)]', + 'Annotated[ ClassVar[int], (3, 5)]', + 'typing.Annotated[ClassVar[int], (3, 5)]', + 'typing .Annotated[ClassVar[int], (3, 5)]', + 'typing. Annotated[ClassVar[int], (3, 5)]', # Not syntactically valid, but these will # be treated as ClassVars. @@ -3642,17 +3663,22 @@ 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 [int]', 'InitVar', - ' InitVar ', 'dataclasses.InitVar[int]', 'dataclasses.InitVar[str]', - ' dataclasses.InitVar[str]', 'dataclasses .InitVar[str]', 'dataclasses. InitVar[str]', 'dataclasses.InitVar [str]', 'dataclasses.InitVar [ str]', + 'Annotated[InitVar[int], (3, 5)]', + 'Annotated[Annotated[InitVar[int], (3, 5)], (3, 6)]', + 'Annotated[dataclasses.InitVar[int], (3, 5)]', + 'Annotated [InitVar[int], (3, 5)]', + 'Annotated[ InitVar[int], (3, 5)]', + 'typing.Annotated[InitVar[int], (3, 5)]', + 'typing .Annotated[InitVar[int], (3, 5)]', + 'typing. Annotated[InitVar[int], (3, 5)]', # Not syntactically valid, but these will # be treated as InitVars. diff --git a/Misc/NEWS.d/next/Library/2022-01-28-21-27-09.bpo-46511.092D9i.rst b/Misc/NEWS.d/next/Library/2022-01-28-21-27-09.bpo-46511.092D9i.rst new file mode 100644 index 00000000000000..00f57d68db061e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-01-28-21-27-09.bpo-46511.092D9i.rst @@ -0,0 +1 @@ +Allow :data:`typing.Annotated` to wrap :data:`typing.ClassVar` and :data:`dataclasses.InitVar` in dataclasses. Patch by Gregory Beauregard.