Skip to content

Commit

Permalink
Fix ClassVar as string fails when getting type hints (GH-6824)
Browse files Browse the repository at this point in the history
  • Loading branch information
nnja authored and ambv committed May 16, 2018
1 parent 8b94b41 commit 2d2d3b1
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 6 deletions.
24 changes: 24 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 13 additions & 6 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)):
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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__

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix failure in `typing.get_type_hints()` when ClassVar was provided as a string forward reference.

0 comments on commit 2d2d3b1

Please sign in to comment.