diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 314716cd7de354..be768f12fb4c6b 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1599,6 +1599,30 @@ class D(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 + 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_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): @no_type_check_decorator diff --git a/Lib/typing.py b/Lib/typing.py index 8025dfd932624b..b10615c07fbdf9 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: @@ -488,7 +494,8 @@ def _evaluate(self, globalns, localns): localns = globalns self.__forward_value__ = _type_check( eval(self.__forward_code__, globalns, localns), - "Forward references must evaluate to types.") + "Forward references must evaluate to types.", + is_argument=self.__forward_is_argument__) self.__forward_evaluated__ = True return self.__forward_value__ @@ -998,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 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 00000000000000..ba8514cdd8950b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-05-15-18-02-03.bpo-0.pj2Mbb.rst @@ -0,0 +1 @@ +Fix failure in `typing.get_type_hints()` when ClassVar was provided as a string forward reference.