diff --git a/Lib/inspect.py b/Lib/inspect.py index b1f3b9fc8dfb0a..d74444e27bed5c 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -160,6 +160,7 @@ from keyword import iskeyword from operator import attrgetter from collections import namedtuple, OrderedDict +from typing import _rewrite_star_unpack from weakref import ref as make_weakref # Create constants for the compiler flags in Include/code.h @@ -288,8 +289,9 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False): if type_params := getattr(obj, "__type_params__", ()): locals = {param.__name__: param for param in type_params} | locals - return_value = {key: - value if not isinstance(value, str) else eval(value, globals, locals) + return_value = { + key: value if not isinstance(value, str) + else eval(_rewrite_star_unpack(value), globals, locals) for key, value in ann.items() } return return_value diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 3ebb305606211d..a4c5ac2e909c76 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -1859,6 +1859,10 @@ def test_get_annotations_with_stringized_annotations(self): self.assertEqual(inspect.get_annotations(isa2, eval_str=True), {}) self.assertEqual(inspect.get_annotations(isa2, eval_str=False), {}) + def f(*args: *tuple[int, ...]): ... + self.assertEqual(inspect.get_annotations(f, eval_str=True), + {'args': (*tuple[int, ...],)[0]}) + def times_three(fn): @functools.wraps(fn) def wrapper(a, b): diff --git a/Lib/typing.py b/Lib/typing.py index 67b1ef329d5932..cbc6d90e13a830 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1024,15 +1024,8 @@ def __init__(self, arg, is_argument=True, module=None, *, is_class=False): if not isinstance(arg, str): raise TypeError(f"Forward reference must be a string -- got {arg!r}") - # If we do `def f(*args: *Ts)`, then we'll have `arg = '*Ts'`. - # Unfortunately, this isn't a valid expression on its own, so we - # do the unpacking manually. - if arg.startswith('*'): - arg_to_compile = f'({arg},)[0]' # E.g. (*Ts,)[0] or (*tuple[int, int],)[0] - else: - arg_to_compile = arg try: - code = compile(arg_to_compile, '', 'eval') + code = compile(_rewrite_star_unpack(arg), '', 'eval') except SyntaxError: raise SyntaxError(f"Forward reference must be an expression -- got {arg!r}") @@ -1119,6 +1112,16 @@ def __repr__(self): return f'ForwardRef({self.__forward_arg__!r}{module_repr})' +def _rewrite_star_unpack(arg): + """If the given argument annotation expression is a star unpack e.g. `'*Ts'` + rewrite it to a valid expression. + """ + if arg.startswith("*"): + return f"({arg},)[0]" # E.g. (*Ts,)[0] or (*tuple[int, int],)[0] + else: + return arg + + def _is_unpacked_typevartuple(x: Any) -> bool: return ((not isinstance(x, type)) and getattr(x, '__typing_is_unpacked_typevartuple__', False)) diff --git a/Misc/NEWS.d/next/Library/2025-09-15-21-03-11.gh-issue-138891.oZFdtR.rst b/Misc/NEWS.d/next/Library/2025-09-15-21-03-11.gh-issue-138891.oZFdtR.rst new file mode 100644 index 00000000000000..f7ecb05d20c241 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-15-21-03-11.gh-issue-138891.oZFdtR.rst @@ -0,0 +1,2 @@ +Fix ``SyntaxError`` when ``inspect.get_annotations(f, eval_str=True)`` is +called on a function annotated with a :pep:`646` ``star_expression``