Skip to content

Commit 784905d

Browse files
sobolevnambvFidget-Spinner
authored
bpo-45166: fixes get_type_hints failure on Final (GH-28279)
Co-authored-by: Łukasz Langa <[email protected]> Co-authored-by: Ken Jin <[email protected]>
1 parent 4c0fc65 commit 784905d

File tree

5 files changed

+56
-10
lines changed

5 files changed

+56
-10
lines changed

Lib/test/ann_module5.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Used by test_typing to verify that Final wrapped in ForwardRef works.
2+
3+
from __future__ import annotations
4+
5+
from typing import Final
6+
7+
name: Final[str] = "final"
8+
9+
class MyClass:
10+
value: Final = 3000

Lib/test/ann_module6.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Tests that top-level ClassVar is not allowed
2+
3+
from __future__ import annotations
4+
5+
from typing import ClassVar
6+
7+
wrong: ClassVar[int] = 1

Lib/test/test_typing.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2975,7 +2975,7 @@ async def __aexit__(self, etype, eval, tb):
29752975

29762976
# Definitions needed for features introduced in Python 3.6
29772977

2978-
from test import ann_module, ann_module2, ann_module3
2978+
from test import ann_module, ann_module2, ann_module3, ann_module5, ann_module6
29792979
from typing import AsyncContextManager
29802980

29812981
class A:
@@ -3339,6 +3339,22 @@ class C(Generic[T]): pass
33393339
(Concatenate[int, P], int))
33403340
self.assertEqual(get_args(list | str), (list, str))
33413341

3342+
def test_forward_ref_and_final(self):
3343+
# https://bugs.python.org/issue45166
3344+
hints = get_type_hints(ann_module5)
3345+
self.assertEqual(hints, {'name': Final[str]})
3346+
3347+
hints = get_type_hints(ann_module5.MyClass)
3348+
self.assertEqual(hints, {'value': Final})
3349+
3350+
def test_top_level_class_var(self):
3351+
# https://bugs.python.org/issue45166
3352+
with self.assertRaisesRegex(
3353+
TypeError,
3354+
r'typing.ClassVar\[int\] is not valid as type argument',
3355+
):
3356+
get_type_hints(ann_module6)
3357+
33423358

33433359
class CollectionsAbcTests(BaseTestCase):
33443360

Lib/typing.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def _type_convert(arg, module=None):
151151
return arg
152152

153153

154-
def _type_check(arg, msg, is_argument=True, module=None):
154+
def _type_check(arg, msg, is_argument=True, module=None, *, is_class=False):
155155
"""Check that the argument is a type, and return it (internal helper).
156156
157157
As a special case, accept None and return type(None) instead. Also wrap strings
@@ -164,14 +164,16 @@ def _type_check(arg, msg, is_argument=True, module=None):
164164
We append the repr() of the actual value (truncated to 100 chars).
165165
"""
166166
invalid_generic_forms = (Generic, Protocol)
167-
if is_argument:
168-
invalid_generic_forms = invalid_generic_forms + (ClassVar, Final)
167+
if not is_class:
168+
invalid_generic_forms += (ClassVar,)
169+
if is_argument:
170+
invalid_generic_forms += (Final,)
169171

170172
arg = _type_convert(arg, module=module)
171173
if (isinstance(arg, _GenericAlias) and
172174
arg.__origin__ in invalid_generic_forms):
173175
raise TypeError(f"{arg} is not valid as type argument")
174-
if arg in (Any, NoReturn):
176+
if arg in (Any, NoReturn, Final):
175177
return arg
176178
if isinstance(arg, _SpecialForm) or arg in (Generic, Protocol):
177179
raise TypeError(f"Plain {arg} is not valid as type argument")
@@ -662,9 +664,10 @@ class ForwardRef(_Final, _root=True):
662664

663665
__slots__ = ('__forward_arg__', '__forward_code__',
664666
'__forward_evaluated__', '__forward_value__',
665-
'__forward_is_argument__', '__forward_module__')
667+
'__forward_is_argument__', '__forward_is_class__',
668+
'__forward_module__')
666669

667-
def __init__(self, arg, is_argument=True, module=None):
670+
def __init__(self, arg, is_argument=True, module=None, *, is_class=False):
668671
if not isinstance(arg, str):
669672
raise TypeError(f"Forward reference must be a string -- got {arg!r}")
670673
try:
@@ -676,6 +679,7 @@ def __init__(self, arg, is_argument=True, module=None):
676679
self.__forward_evaluated__ = False
677680
self.__forward_value__ = None
678681
self.__forward_is_argument__ = is_argument
682+
self.__forward_is_class__ = is_class
679683
self.__forward_module__ = module
680684

681685
def _evaluate(self, globalns, localns, recursive_guard):
@@ -692,10 +696,11 @@ def _evaluate(self, globalns, localns, recursive_guard):
692696
globalns = getattr(
693697
sys.modules.get(self.__forward_module__, None), '__dict__', globalns
694698
)
695-
type_ =_type_check(
699+
type_ = _type_check(
696700
eval(self.__forward_code__, globalns, localns),
697701
"Forward references must evaluate to types.",
698702
is_argument=self.__forward_is_argument__,
703+
is_class=self.__forward_is_class__,
699704
)
700705
self.__forward_value__ = _eval_type(
701706
type_, globalns, localns, recursive_guard | {self.__forward_arg__}
@@ -1799,7 +1804,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
17991804
if value is None:
18001805
value = type(None)
18011806
if isinstance(value, str):
1802-
value = ForwardRef(value, is_argument=False)
1807+
value = ForwardRef(value, is_argument=False, is_class=True)
18031808
value = _eval_type(value, base_globals, base_locals)
18041809
hints[name] = value
18051810
return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}
@@ -1831,7 +1836,13 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
18311836
if value is None:
18321837
value = type(None)
18331838
if isinstance(value, str):
1834-
value = ForwardRef(value)
1839+
# class-level forward refs were handled above, this must be either
1840+
# a module-level annotation or a function argument annotation
1841+
value = ForwardRef(
1842+
value,
1843+
is_argument=not isinstance(obj, types.ModuleType),
1844+
is_class=False,
1845+
)
18351846
value = _eval_type(value, globalns, localns)
18361847
if name in defaults and defaults[name] is None:
18371848
value = Optional[value]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:func:`typing.get_type_hints` now works with :data:`~typing.Final` wrapped in
2+
:class:`~typing.ForwardRef`.

0 commit comments

Comments
 (0)