diff --git a/pyupgrade/_plugins/typing_pep563.py b/pyupgrade/_plugins/typing_pep563.py index 0eec8af1..5b16f193 100644 --- a/pyupgrade/_plugins/typing_pep563.py +++ b/pyupgrade/_plugins/typing_pep563.py @@ -176,3 +176,16 @@ def visit_AnnAssign( if not _supported_version(state): return yield from _replace_string_literal(node.annotation) + + +if sys.version_info >= (3, 12): # pragma: >=3.12 cover + @register(ast.TypeVar) + def visit_TypeVar( + state: State, + node: ast.TypeVar, + parent: ast.AST, + ) -> Iterable[tuple[Offset, TokenFunc]]: + if not _supported_version(state): + return + if node.bound is not None: + yield from _replace_string_literal(node.bound) diff --git a/tests/features/typing_pep563_test.py b/tests/features/typing_pep563_test.py index 85f7a419..59e77801 100644 --- a/tests/features/typing_pep563_test.py +++ b/tests/features/typing_pep563_test.py @@ -1,5 +1,7 @@ from __future__ import annotations +import sys + import pytest from pyupgrade._data import Settings @@ -54,6 +56,15 @@ 'x: Annotated[1:2] = ...\n', id='Annotated with invalid slice', ), + pytest.param( + 'def f[X: "int"](x: X) -> X: return x\n', + id='TypeVar quoted bound but no __future__ annotations', + ), + pytest.param( + 'from __future__ import annotations\n' + 'def f[X](x: X) -> X: return x\n', + id='TypeVar without bound', + ), ), ) def test_fix_typing_pep563_noop(s): @@ -363,3 +374,19 @@ def test_fix_typing_pep563_noop(s): def test_fix_typing_pep563(s, expected): ret = _fix_plugins(s, settings=Settings(min_version=(3, 7))) assert ret == expected + + +@pytest.mark.xfail(sys.version_info < (3, 12), reason='3.12+ syntax') +def test_typevar_bound(): + src = '''\ +from __future__ import annotations +def f[T: "int"](t: T) -> T: + return t +''' + expected = '''\ +from __future__ import annotations +def f[T: int](t: T) -> T: + return t +''' + ret = _fix_plugins(src, settings=Settings()) + assert ret == expected