Skip to content

bpo-43224: Implement PEP 646 grammar changes #31018

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Mar 26, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Fix typing.get_type_hints on variadic annotation
  • Loading branch information
mrahtz committed Mar 24, 2022
commit fbfd5b142ee2bb4f136fabd0b38946b6b42fb02c
21 changes: 20 additions & 1 deletion Lib/test/test_future.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def _exec_future(self, code):
scope = {}
exec(
"from __future__ import annotations\n"
+ code, {}, scope
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this because if both globals and locals are passed to exec, it treats the code as it if were executed in a class definition, which makes the new test I've added harder than it needs to be (we can't access c so easily). Removing this doesn't seem to break any of the rest of the tests, so I hope this is fine?

+ code, scope
)
return scope

Expand Down Expand Up @@ -419,6 +419,25 @@ def foo():
def bar(arg: (yield)): pass
"""))

def test_get_type_hints_on_func_with_variadic_arg(self):
# `typing.get_type_hints` might break on a function with a variadic
# annotation (e.g. `f(*args: *Ts)`) if `from __future__ import
# annotations`, because it could try to evaluate `*Ts` as en expression,
# which on its own isn't value syntax.
namespace = self._exec_future(dedent("""\
class StarredC: pass
class C:
def __iter__(self):
yield StarredC()
c = C()
def f(*args: *c): pass
import typing
hints = typing.get_type_hints(f)
"""))

hints = namespace.pop('hints')
self.assertIsInstance(hints['args'], namespace['StarredC'])


if __name__ == "__main__":
unittest.main()
12 changes: 11 additions & 1 deletion Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,10 +734,20 @@ class ForwardRef(_Final, _root=True):
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[0] == '*':
arg_to_compile = f'next(iter({arg[1:]}))'
else:
arg_to_compile = arg
print(f"arg_to_compile: '{arg_to_compile}'")
try:
code = compile(arg, '<string>', 'eval')
code = compile(arg_to_compile, '<string>', 'eval')
except SyntaxError:
raise SyntaxError(f"Forward reference must be an expression -- got {arg!r}")

self.__forward_arg__ = arg
self.__forward_code__ = code
self.__forward_evaluated__ = False
Expand Down