From d13c7412c9b12b9a8feafeb23b53c23367c5c3ea Mon Sep 17 00:00:00 2001 From: Nina Zakharenko Date: Mon, 14 May 2018 15:28:51 -0400 Subject: [PATCH 1/5] Declaring a ClassVar with a type as a string shouldn't fail at runtime. See python/typing#505 --- Lib/test/test_typing.py | 22 ++++++++++++++-------- Lib/typing.py | 4 +--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 314716cd7de3545..eba57a70ba0cd4c 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1553,14 +1553,6 @@ def foo(a: 'Node[T'): with self.assertRaises(SyntaxError): get_type_hints(foo) - def test_type_error(self): - - def foo(a: Tuple['42']): - pass - - with self.assertRaises(TypeError): - get_type_hints(foo) - def test_name_error(self): def foo(a: 'Noode[T]'): @@ -1590,6 +1582,20 @@ def foo(a: 'whatevers') -> {}: ith = get_type_hints(C().foo) self.assertEqual(ith, {}) + def test_no_type_check_forward_ref_as_string(self): + class C: + foo: typing.ClassVar[int] = 7 + class D: + foo: ClassVar[int] = 7 + class E: + foo: 'typing.ClassVar[int]' = 7 + class F: + foo: 'ClassVar[int]' = 7 + + expected_result = {'foo': typing.ClassVar[int]} + for clazz in [C, D, E, F]: + self.assertEqual(get_type_hints(clazz), expected_result) + def test_no_type_check_no_bases(self): class C: def meth(self, x: int): ... diff --git a/Lib/typing.py b/Lib/typing.py index 8025dfd932624b6..bbcae38761a8342 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -486,9 +486,7 @@ def _evaluate(self, globalns, localns): globalns = localns elif localns is None: localns = globalns - self.__forward_value__ = _type_check( - eval(self.__forward_code__, globalns, localns), - "Forward references must evaluate to types.") + self.__forward_value__ = eval(self.__forward_code__, globalns, localns) self.__forward_evaluated__ = True return self.__forward_value__ From b8f0472219b52563653a5d7808fc390ab31b2f04 Mon Sep 17 00:00:00 2001 From: Nina Zakharenko Date: Tue, 15 May 2018 14:31:52 -0400 Subject: [PATCH 2/5] Address comments from PR. Add additional unit test to test nested ClassVar scenario. --- Lib/test/test_typing.py | 26 ++++++++++++++++++-------- Lib/typing.py | 21 +++++++++++++++------ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index eba57a70ba0cd4c..eccc17ce074b33a 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1582,6 +1582,15 @@ def foo(a: 'whatevers') -> {}: ith = get_type_hints(C().foo) self.assertEqual(ith, {}) + def test_no_type_check_no_bases(self): + class C: + def meth(self, x: int): ... + @no_type_check + class D(C): + c = C + # verify that @no_type_check never affects bases + self.assertEqual(get_type_hints(C.meth), {'x': int}) + def test_no_type_check_forward_ref_as_string(self): class C: foo: typing.ClassVar[int] = 7 @@ -1596,14 +1605,15 @@ class F: for clazz in [C, D, E, F]: self.assertEqual(get_type_hints(clazz), expected_result) - def test_no_type_check_no_bases(self): - class C: - def meth(self, x: int): ... - @no_type_check - class D(C): - c = C - # verify that @no_type_check never affects bases - self.assertEqual(get_type_hints(C.meth), {'x': int}) + def test_nested_classvar_fails_forward_ref_check(self): + class E: + foo: 'typing.ClassVar[typing.ClassVar[int]]' = 7 + class F: + foo: ClassVar['ClassVar[int]'] = 7 + + for clazz in [E, F]: + with self.assertRaises(TypeError): + get_type_hints(clazz) def test_meta_no_type_check(self): diff --git a/Lib/typing.py b/Lib/typing.py index bbcae38761a8342..b10615c07fbdf93 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -106,7 +106,7 @@ # legitimate imports of those modules. -def _type_check(arg, msg): +def _type_check(arg, msg, is_argument=False): """Check that the argument is a type, and return it (internal helper). As a special case, accept None and return type(None) instead. Also wrap strings @@ -118,12 +118,16 @@ def _type_check(arg, msg): We append the repr() of the actual value (truncated to 100 chars). """ + invalid_generic_forms = (Generic, _Protocol) + if not is_argument: + invalid_generic_forms = invalid_generic_forms + (ClassVar, ) + if arg is None: return type(None) if isinstance(arg, str): return ForwardRef(arg) if (isinstance(arg, _GenericAlias) and - arg.__origin__ in (Generic, _Protocol, ClassVar)): + arg.__origin__ in invalid_generic_forms): raise TypeError(f"{arg} is not valid as type argument") if (isinstance(arg, _SpecialForm) and arg is not Any or arg in (Generic, _Protocol)): @@ -464,9 +468,10 @@ class ForwardRef(_Final, _root=True): """Internal wrapper to hold a forward reference.""" __slots__ = ('__forward_arg__', '__forward_code__', - '__forward_evaluated__', '__forward_value__') + '__forward_evaluated__', '__forward_value__', + '__forward_is_argument__') - def __init__(self, arg): + def __init__(self, arg, is_argument=False): if not isinstance(arg, str): raise TypeError(f"Forward reference must be a string -- got {arg!r}") try: @@ -477,6 +482,7 @@ def __init__(self, arg): self.__forward_code__ = code self.__forward_evaluated__ = False self.__forward_value__ = None + self.__forward_is_argument__ = is_argument def _evaluate(self, globalns, localns): if not self.__forward_evaluated__ or localns is not globalns: @@ -486,7 +492,10 @@ def _evaluate(self, globalns, localns): globalns = localns elif localns is None: localns = globalns - self.__forward_value__ = eval(self.__forward_code__, globalns, localns) + self.__forward_value__ = _type_check( + eval(self.__forward_code__, globalns, localns), + "Forward references must evaluate to types.", + is_argument=self.__forward_is_argument__) self.__forward_evaluated__ = True return self.__forward_value__ @@ -996,7 +1005,7 @@ def get_type_hints(obj, globalns=None, localns=None): if value is None: value = type(None) if isinstance(value, str): - value = ForwardRef(value) + value = ForwardRef(value, is_argument=True) value = _eval_type(value, base_globals, localns) hints[name] = value return hints From 79bfabb70256455c42f7808580fa404ab273ae74 Mon Sep 17 00:00:00 2001 From: Nina Zakharenko Date: Tue, 15 May 2018 17:03:10 -0400 Subject: [PATCH 3/5] Add back a test that was accidentally removed --- Lib/test/test_typing.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index eccc17ce074b33a..be768f12fb4c6be 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1553,6 +1553,14 @@ def foo(a: 'Node[T'): with self.assertRaises(SyntaxError): get_type_hints(foo) + def test_type_error(self): + + def foo(a: Tuple['42']): + pass + + with self.assertRaises(TypeError): + get_type_hints(foo) + def test_name_error(self): def foo(a: 'Noode[T]'): From 5eb9b734eb430ad3e956271bcc101bacef405cee Mon Sep 17 00:00:00 2001 From: Nina Zakharenko Date: Tue, 15 May 2018 18:02:21 -0400 Subject: [PATCH 4/5] Add NEWS entry --- Misc/NEWS.d/next/Library/2018-05-15-18-02-03.bpo-0.pj2Mbb.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2018-05-15-18-02-03.bpo-0.pj2Mbb.rst diff --git a/Misc/NEWS.d/next/Library/2018-05-15-18-02-03.bpo-0.pj2Mbb.rst b/Misc/NEWS.d/next/Library/2018-05-15-18-02-03.bpo-0.pj2Mbb.rst new file mode 100644 index 000000000000000..0a5f293942fe7db --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-05-15-18-02-03.bpo-0.pj2Mbb.rst @@ -0,0 +1 @@ +Fix ClassVar as string failure when getting type hints From a053fcd37a29ed08db25c090f5c02e15c043d432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 15 May 2018 22:09:42 -0400 Subject: [PATCH 5/5] Update 2018-05-15-18-02-03.bpo-0.pj2Mbb.rst --- Misc/NEWS.d/next/Library/2018-05-15-18-02-03.bpo-0.pj2Mbb.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2018-05-15-18-02-03.bpo-0.pj2Mbb.rst b/Misc/NEWS.d/next/Library/2018-05-15-18-02-03.bpo-0.pj2Mbb.rst index 0a5f293942fe7db..ba8514cdd8950b0 100644 --- a/Misc/NEWS.d/next/Library/2018-05-15-18-02-03.bpo-0.pj2Mbb.rst +++ b/Misc/NEWS.d/next/Library/2018-05-15-18-02-03.bpo-0.pj2Mbb.rst @@ -1 +1 @@ -Fix ClassVar as string failure when getting type hints +Fix failure in `typing.get_type_hints()` when ClassVar was provided as a string forward reference.