Skip to content

Commit 1f08d16

Browse files
ambvsobolevn
andauthored
[3.9] bpo-45166: fixes get_type_hints failure on Final (GH-28279) (GH-28561)
Co-authored-by: Łukasz Langa <[email protected]> Co-authored-by: Ken Jin <[email protected]>. (cherry picked from commit 784905d) Co-authored-by: Nikita Sobolev <[email protected]>
1 parent 2a7d985 commit 1f08d16

File tree

5 files changed

+57
-11
lines changed

5 files changed

+57
-11
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
@@ -2808,7 +2808,7 @@ async def __aexit__(self, etype, eval, tb):
28082808

28092809
# Definitions needed for features introduced in Python 3.6
28102810

2811-
from test import ann_module, ann_module2, ann_module3
2811+
from test import ann_module, ann_module2, ann_module3, ann_module5, ann_module6
28122812
from typing import AsyncContextManager
28132813

28142814
class A:
@@ -3128,6 +3128,22 @@ class C(Generic[T]): pass
31283128
self.assertEqual(get_args(collections.abc.Callable[[int], str]),
31293129
get_args(Callable[[int], str]))
31303130

3131+
def test_forward_ref_and_final(self):
3132+
# https://bugs.python.org/issue45166
3133+
hints = get_type_hints(ann_module5)
3134+
self.assertEqual(hints, {'name': Final[str]})
3135+
3136+
hints = get_type_hints(ann_module5.MyClass)
3137+
self.assertEqual(hints, {'value': Final})
3138+
3139+
def test_top_level_class_var(self):
3140+
# https://bugs.python.org/issue45166
3141+
with self.assertRaisesRegex(
3142+
TypeError,
3143+
r'typing.ClassVar\[int\] is not valid as type argument',
3144+
):
3145+
get_type_hints(ann_module6)
3146+
31313147

31323148
class CollectionsAbcTests(BaseTestCase):
31333149

Lib/typing.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
'NamedTuple', # Not really a type.
9696
'TypedDict', # Not really a type.
9797
'Generator',
98-
98+
9999
# Other concrete types.
100100
'BinaryIO',
101101
'IO',
@@ -134,7 +134,7 @@ def _type_convert(arg, module=None):
134134
return arg
135135

136136

137-
def _type_check(arg, msg, is_argument=True, module=None):
137+
def _type_check(arg, msg, is_argument=True, module=None, *, is_class=False):
138138
"""Check that the argument is a type, and return it (internal helper).
139139
140140
As a special case, accept None and return type(None) instead. Also wrap strings
@@ -147,14 +147,16 @@ def _type_check(arg, msg, is_argument=True, module=None):
147147
We append the repr() of the actual value (truncated to 100 chars).
148148
"""
149149
invalid_generic_forms = (Generic, Protocol)
150-
if is_argument:
151-
invalid_generic_forms = invalid_generic_forms + (ClassVar, Final)
150+
if not is_class:
151+
invalid_generic_forms += (ClassVar,)
152+
if is_argument:
153+
invalid_generic_forms += (Final,)
152154

153155
arg = _type_convert(arg, module=module)
154156
if (isinstance(arg, _GenericAlias) and
155157
arg.__origin__ in invalid_generic_forms):
156158
raise TypeError(f"{arg} is not valid as type argument")
157-
if arg in (Any, NoReturn):
159+
if arg in (Any, NoReturn, Final):
158160
return arg
159161
if isinstance(arg, _SpecialForm) or arg in (Generic, Protocol):
160162
raise TypeError(f"Plain {arg} is not valid as type argument")
@@ -517,9 +519,10 @@ class ForwardRef(_Final, _root=True):
517519

518520
__slots__ = ('__forward_arg__', '__forward_code__',
519521
'__forward_evaluated__', '__forward_value__',
520-
'__forward_is_argument__', '__forward_module__')
522+
'__forward_is_argument__', '__forward_is_class__',
523+
'__forward_module__')
521524

522-
def __init__(self, arg, is_argument=True, module=None):
525+
def __init__(self, arg, is_argument=True, module=None, *, is_class=False):
523526
if not isinstance(arg, str):
524527
raise TypeError(f"Forward reference must be a string -- got {arg!r}")
525528
try:
@@ -531,6 +534,7 @@ def __init__(self, arg, is_argument=True, module=None):
531534
self.__forward_evaluated__ = False
532535
self.__forward_value__ = None
533536
self.__forward_is_argument__ = is_argument
537+
self.__forward_is_class__ = is_class
534538
self.__forward_module__ = module
535539

536540
def _evaluate(self, globalns, localns, recursive_guard):
@@ -547,10 +551,11 @@ def _evaluate(self, globalns, localns, recursive_guard):
547551
globalns = getattr(
548552
sys.modules.get(self.__forward_module__, None), '__dict__', globalns
549553
)
550-
type_ =_type_check(
554+
type_ = _type_check(
551555
eval(self.__forward_code__, globalns, localns),
552556
"Forward references must evaluate to types.",
553557
is_argument=self.__forward_is_argument__,
558+
is_class=self.__forward_is_class__,
554559
)
555560
self.__forward_value__ = _eval_type(
556561
type_, globalns, localns, recursive_guard | {self.__forward_arg__}
@@ -1450,7 +1455,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
14501455
if value is None:
14511456
value = type(None)
14521457
if isinstance(value, str):
1453-
value = ForwardRef(value, is_argument=False)
1458+
value = ForwardRef(value, is_argument=False, is_class=True)
14541459
value = _eval_type(value, base_globals, localns)
14551460
hints[name] = value
14561461
return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}
@@ -1482,7 +1487,13 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
14821487
if value is None:
14831488
value = type(None)
14841489
if isinstance(value, str):
1485-
value = ForwardRef(value)
1490+
# class-level forward refs were handled above, this must be either
1491+
# a module-level annotation or a function argument annotation
1492+
value = ForwardRef(
1493+
value,
1494+
is_argument=not isinstance(obj, types.ModuleType),
1495+
is_class=False,
1496+
)
14861497
value = _eval_type(value, globalns, localns)
14871498
if name in defaults and defaults[name] is None:
14881499
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)