diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3b225b73..804dc78b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,10 +10,10 @@ jobs: main-windows: uses: asottile/workflows/.github/workflows/tox.yml@v1.6.0 with: - env: '["py38"]' + env: '["py39"]' os: windows-latest main-linux: uses: asottile/workflows/.github/workflows/tox.yml@v1.6.0 with: - env: '["py38", "py39", "py310", "py311", "py312"]' + env: '["py39", "py310", "py311", "py312"]' os: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a0e9d2be..49c1fae6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -14,28 +14,28 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.12.0 + rev: v3.13.0 hooks: - id: reorder-python-imports - args: [--py38-plus, --add-import, 'from __future__ import annotations'] + args: [--py39-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v3.1.0 hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.19.0 hooks: - id: pyupgrade - args: [--py38-plus] + args: [--py39-plus] - repo: https://github.com/hhatto/autopep8 - rev: v2.0.4 + rev: v2.3.1 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 + rev: 7.1.1 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.11.2 hooks: - id: mypy diff --git a/README.md b/README.md index 1ad0c0a0..35f36f89 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.19.0 hooks: - id: pyupgrade ``` @@ -555,7 +555,7 @@ Note that `if` blocks without an `else` will not be rewritten as it could introd Availability: - `--py310-plus` for `socket.timeout` -- `--py311-plus` for `asyncio.Timeout` +- `--py311-plus` for `asyncio.TimeoutError` ```diff @@ -754,6 +754,24 @@ Availability: ... ``` +### pep 696 TypeVar defaults + +Availability: +- File imports `from __future__ import annotations` + - Unless `--keep-runtime-typing` is passed on the commandline. +- `--py313-plus` is passed on the commandline. + +```diff +-def f() -> Generator[int, None, None]: ++def f() -> Generator[int]: + yield 1 +``` + +```diff +-async def f() -> AsyncGenerator[int, None]: ++async def f() -> AsyncGenerator[int]: + yield 1 +``` ### remove quoted annotations diff --git a/pyupgrade/_ast_helpers.py b/pyupgrade/_ast_helpers.py index 8a79d3ab..9763d060 100644 --- a/pyupgrade/_ast_helpers.py +++ b/pyupgrade/_ast_helpers.py @@ -2,7 +2,7 @@ import ast import warnings -from typing import Container +from collections.abc import Container from tokenize_rt import Offset diff --git a/pyupgrade/_data.py b/pyupgrade/_data.py index ab3a4942..164d30b2 100644 --- a/pyupgrade/_data.py +++ b/pyupgrade/_data.py @@ -3,12 +3,10 @@ import ast import collections import pkgutil +from collections.abc import Iterable from typing import Callable -from typing import Iterable -from typing import List from typing import NamedTuple from typing import Protocol -from typing import Tuple from typing import TypeVar from tokenize_rt import Offset @@ -16,7 +14,7 @@ from pyupgrade import _plugins -Version = Tuple[int, ...] +Version = tuple[int, ...] class Settings(NamedTuple): @@ -33,13 +31,14 @@ class State(NamedTuple): AST_T = TypeVar('AST_T', bound=ast.AST) -TokenFunc = Callable[[int, List[Token]], None] -ASTFunc = Callable[[State, AST_T, ast.AST], Iterable[Tuple[Offset, TokenFunc]]] +TokenFunc = Callable[[int, list[Token]], None] +ASTFunc = Callable[[State, AST_T, ast.AST], Iterable[tuple[Offset, TokenFunc]]] RECORD_FROM_IMPORTS = frozenset(( '__future__', 'asyncio', 'collections', + 'collections.abc', 'functools', 'mmap', 'os', @@ -53,7 +52,8 @@ class State(NamedTuple): 'typing_extensions', )) -FUNCS = collections.defaultdict(list) +FUNCS: ASTCallbackMapping # python/mypy#17566 +FUNCS = collections.defaultdict(list) # type: ignore[assignment] def register(tp: type[AST_T]) -> Callable[[ASTFunc[AST_T]], ASTFunc[AST_T]]: diff --git a/pyupgrade/_main.py b/pyupgrade/_main.py index c8342031..e52b9c66 100644 --- a/pyupgrade/_main.py +++ b/pyupgrade/_main.py @@ -5,8 +5,8 @@ import re import sys import tokenize -from typing import Match -from typing import Sequence +from collections.abc import Sequence +from re import Match from tokenize_rt import NON_CODING_TOKENS from tokenize_rt import parse_string_literal @@ -378,6 +378,10 @@ def main(argv: Sequence[str] | None = None) -> int: '--py312-plus', action='store_const', dest='min_version', const=(3, 12), ) + parser.add_argument( + '--py313-plus', + action='store_const', dest='min_version', const=(3, 13), + ) args = parser.parse_args(argv) ret = 0 diff --git a/pyupgrade/_plugins/collections_abc.py b/pyupgrade/_plugins/collections_abc.py index f7e879e3..a4bcb10b 100644 --- a/pyupgrade/_plugins/collections_abc.py +++ b/pyupgrade/_plugins/collections_abc.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset diff --git a/pyupgrade/_plugins/constant_fold.py b/pyupgrade/_plugins/constant_fold.py index 07c3de54..5411b95f 100644 --- a/pyupgrade/_plugins/constant_fold.py +++ b/pyupgrade/_plugins/constant_fold.py @@ -1,7 +1,7 @@ from __future__ import annotations import ast -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset diff --git a/pyupgrade/_plugins/datetime_utc_alias.py b/pyupgrade/_plugins/datetime_utc_alias.py index 5280e5db..8c511533 100644 --- a/pyupgrade/_plugins/datetime_utc_alias.py +++ b/pyupgrade/_plugins/datetime_utc_alias.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset diff --git a/pyupgrade/_plugins/defauldict_lambda.py b/pyupgrade/_plugins/defauldict_lambda.py index 1cfa7464..1d6bb0a9 100644 --- a/pyupgrade/_plugins/defauldict_lambda.py +++ b/pyupgrade/_plugins/defauldict_lambda.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import Token diff --git a/pyupgrade/_plugins/default_encoding.py b/pyupgrade/_plugins/default_encoding.py index ab24b9ca..f681510d 100644 --- a/pyupgrade/_plugins/default_encoding.py +++ b/pyupgrade/_plugins/default_encoding.py @@ -1,7 +1,7 @@ from __future__ import annotations import ast -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import Token diff --git a/pyupgrade/_plugins/dict_literals.py b/pyupgrade/_plugins/dict_literals.py index c87e9e89..1577dc8f 100644 --- a/pyupgrade/_plugins/dict_literals.py +++ b/pyupgrade/_plugins/dict_literals.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import Token @@ -53,6 +53,7 @@ def visit_Call( parent: ast.AST, ) -> Iterable[tuple[Offset, TokenFunc]]: if ( + not isinstance(parent, ast.FormattedValue) and isinstance(node.func, ast.Name) and node.func.id == 'dict' and len(node.args) == 1 and diff --git a/pyupgrade/_plugins/exceptions.py b/pyupgrade/_plugins/exceptions.py index 37b39faa..4018ebbb 100644 --- a/pyupgrade/_plugins/exceptions.py +++ b/pyupgrade/_plugins/exceptions.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from typing import NamedTuple from tokenize_rt import Offset diff --git a/pyupgrade/_plugins/format_locals.py b/pyupgrade/_plugins/format_locals.py index cc63c442..af4ae475 100644 --- a/pyupgrade/_plugins/format_locals.py +++ b/pyupgrade/_plugins/format_locals.py @@ -1,7 +1,7 @@ from __future__ import annotations import ast -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import rfind_string_parts diff --git a/pyupgrade/_plugins/fstrings.py b/pyupgrade/_plugins/fstrings.py index e6480756..d9359650 100644 --- a/pyupgrade/_plugins/fstrings.py +++ b/pyupgrade/_plugins/fstrings.py @@ -1,7 +1,7 @@ from __future__ import annotations import ast -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import parse_string_literal diff --git a/pyupgrade/_plugins/identity_equality.py b/pyupgrade/_plugins/identity_equality.py index 2f0d5fd4..9a23c5ec 100644 --- a/pyupgrade/_plugins/identity_equality.py +++ b/pyupgrade/_plugins/identity_equality.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import Token diff --git a/pyupgrade/_plugins/imports.py b/pyupgrade/_plugins/imports.py index ce032f3e..cb7ae93b 100644 --- a/pyupgrade/_plugins/imports.py +++ b/pyupgrade/_plugins/imports.py @@ -4,8 +4,8 @@ import bisect import collections import functools -from typing import Iterable -from typing import Mapping +from collections.abc import Iterable +from collections.abc import Mapping from typing import NamedTuple from tokenize_rt import Offset @@ -21,7 +21,7 @@ from pyupgrade._token_helpers import indented_amount # GENERATED VIA generate-imports -# Using reorder-python-imports==3.12.0 +# Using reorder-python-imports==3.14.0 REMOVALS = { (3,): { '__future__': { @@ -99,12 +99,10 @@ ('typing_extensions', 'ClassVar'): 'typing', ('typing_extensions', 'Collection'): 'typing', ('typing_extensions', 'Container'): 'typing', - ('typing_extensions', 'ContextManager'): 'typing', ('typing_extensions', 'Coroutine'): 'typing', ('typing_extensions', 'DefaultDict'): 'typing', ('typing_extensions', 'Dict'): 'typing', ('typing_extensions', 'FrozenSet'): 'typing', - ('typing_extensions', 'Generator'): 'typing', ('typing_extensions', 'Generic'): 'typing', ('typing_extensions', 'Hashable'): 'typing', ('typing_extensions', 'IO'): 'typing', @@ -138,8 +136,6 @@ }, (3, 7): { ('mypy_extensions', 'NoReturn'): 'typing', - ('typing_extensions', 'AsyncContextManager'): 'typing', - ('typing_extensions', 'AsyncGenerator'): 'typing', ('typing_extensions', 'ChainMap'): 'typing', ('typing_extensions', 'Counter'): 'typing', ('typing_extensions', 'Deque'): 'typing', @@ -150,8 +146,6 @@ ('mypy_extensions', 'TypedDict'): 'typing', ('typing_extensions', 'Final'): 'typing', ('typing_extensions', 'OrderedDict'): 'typing', - ('typing_extensions', 'SupportsIndex'): 'typing', - ('typing_extensions', 'runtime_checkable'): 'typing', }, (3, 9): { ('typing', 'AsyncGenerator'): 'collections.abc', @@ -217,19 +211,35 @@ }, (3, 12): { ('typing_extensions', 'NamedTuple'): 'typing', - ('typing_extensions', 'Protocol'): 'typing', ('typing_extensions', 'SupportsAbs'): 'typing', ('typing_extensions', 'SupportsBytes'): 'typing', ('typing_extensions', 'SupportsComplex'): 'typing', ('typing_extensions', 'SupportsFloat'): 'typing', + ('typing_extensions', 'SupportsIndex'): 'typing', ('typing_extensions', 'SupportsInt'): 'typing', ('typing_extensions', 'SupportsRound'): 'typing', ('typing_extensions', 'TypeAliasType'): 'typing', - ('typing_extensions', 'TypedDict'): 'typing', ('typing_extensions', 'Unpack'): 'typing', ('typing_extensions', 'dataclass_transform'): 'typing', ('typing_extensions', 'override'): 'typing', }, + (3, 13): { + ('typing_extensions', 'AsyncContextManager'): 'typing', + ('typing_extensions', 'AsyncGenerator'): 'typing', + ('typing_extensions', 'ContextManager'): 'typing', + ('typing_extensions', 'Generator'): 'typing', + ('typing_extensions', 'NoDefault'): 'typing', + ('typing_extensions', 'ParamSpec'): 'typing', + ('typing_extensions', 'Protocol'): 'typing', + ('typing_extensions', 'ReadOnly'): 'typing', + ('typing_extensions', 'TypeIs'): 'typing', + ('typing_extensions', 'TypeVar'): 'typing', + ('typing_extensions', 'TypeVarTuple'): 'typing', + ('typing_extensions', 'TypedDict'): 'typing', + ('typing_extensions', 'get_protocol_members'): 'typing', + ('typing_extensions', 'is_protocol'): 'typing', + ('typing_extensions', 'runtime_checkable'): 'typing', + }, } REPLACE_MODS = { 'six.moves.BaseHTTPServer': 'http.server', @@ -288,7 +298,7 @@ # END GENERATED -@functools.lru_cache(maxsize=None) +@functools.cache def _for_version( version: tuple[int, ...], *, diff --git a/pyupgrade/_plugins/io_open.py b/pyupgrade/_plugins/io_open.py index 72fe4493..da20a3d4 100644 --- a/pyupgrade/_plugins/io_open.py +++ b/pyupgrade/_plugins/io_open.py @@ -1,7 +1,7 @@ from __future__ import annotations import ast -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import Token diff --git a/pyupgrade/_plugins/legacy.py b/pyupgrade/_plugins/legacy.py index c042a05c..f79219ff 100644 --- a/pyupgrade/_plugins/legacy.py +++ b/pyupgrade/_plugins/legacy.py @@ -4,9 +4,9 @@ import collections import contextlib import functools +from collections.abc import Generator +from collections.abc import Iterable from typing import Any -from typing import Generator -from typing import Iterable from tokenize_rt import Offset from tokenize_rt import Token @@ -91,7 +91,7 @@ def __init__(self) -> None: self.yield_offsets: set[Offset] = set() @contextlib.contextmanager - def _scope(self, node: ast.AST) -> Generator[None, None, None]: + def _scope(self, node: ast.AST) -> Generator[None]: self._scopes.append(Scope(node)) try: yield diff --git a/pyupgrade/_plugins/lru_cache.py b/pyupgrade/_plugins/lru_cache.py index 6d690ecb..cec153f0 100644 --- a/pyupgrade/_plugins/lru_cache.py +++ b/pyupgrade/_plugins/lru_cache.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import Token diff --git a/pyupgrade/_plugins/metaclass_type.py b/pyupgrade/_plugins/metaclass_type.py index 5749050a..0010cf3b 100644 --- a/pyupgrade/_plugins/metaclass_type.py +++ b/pyupgrade/_plugins/metaclass_type.py @@ -1,7 +1,7 @@ from __future__ import annotations import ast -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import Token diff --git a/pyupgrade/_plugins/mock.py b/pyupgrade/_plugins/mock.py index 0e1c391b..bfae7024 100644 --- a/pyupgrade/_plugins/mock.py +++ b/pyupgrade/_plugins/mock.py @@ -1,7 +1,7 @@ from __future__ import annotations import ast -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import Token diff --git a/pyupgrade/_plugins/native_literals.py b/pyupgrade/_plugins/native_literals.py index ba535d64..90d0e390 100644 --- a/pyupgrade/_plugins/native_literals.py +++ b/pyupgrade/_plugins/native_literals.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import Token diff --git a/pyupgrade/_plugins/new_style_classes.py b/pyupgrade/_plugins/new_style_classes.py index f0bb6f4e..6535a5a7 100644 --- a/pyupgrade/_plugins/new_style_classes.py +++ b/pyupgrade/_plugins/new_style_classes.py @@ -1,7 +1,7 @@ from __future__ import annotations import ast -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset diff --git a/pyupgrade/_plugins/open_mode.py b/pyupgrade/_plugins/open_mode.py index f0bfc98c..17730afb 100644 --- a/pyupgrade/_plugins/open_mode.py +++ b/pyupgrade/_plugins/open_mode.py @@ -3,7 +3,7 @@ import ast import functools import itertools -from typing import Iterable +from collections.abc import Iterable from typing import NamedTuple from tokenize_rt import Offset diff --git a/pyupgrade/_plugins/percent_format.py b/pyupgrade/_plugins/percent_format.py index db845e7b..effc4ddd 100644 --- a/pyupgrade/_plugins/percent_format.py +++ b/pyupgrade/_plugins/percent_format.py @@ -3,12 +3,11 @@ import ast import functools import re -from typing import Generator -from typing import Iterable -from typing import Match +from collections.abc import Generator +from collections.abc import Iterable +from re import Match +from re import Pattern from typing import Optional -from typing import Pattern -from typing import Tuple from tokenize_rt import Offset from tokenize_rt import Token @@ -23,14 +22,14 @@ from pyupgrade._token_helpers import remove_brace from pyupgrade._token_helpers import victims -PercentFormatPart = Tuple[ +PercentFormatPart = tuple[ Optional[str], Optional[str], Optional[str], Optional[str], str, ] -PercentFormat = Tuple[str, Optional[PercentFormatPart]] +PercentFormat = tuple[str, Optional[PercentFormatPart]] MAPPING_KEY_RE = re.compile(r'\(([^()]*)\)') CONVERSION_FLAG_RE = re.compile('[#0+ -]*') @@ -46,7 +45,7 @@ def _must_match(regex: Pattern[str], string: str, pos: int) -> Match[str]: def _parse_percent_format(s: str) -> tuple[PercentFormat, ...]: - def _parse_inner() -> Generator[PercentFormat, None, None]: + def _parse_inner() -> Generator[PercentFormat]: string_start = 0 string_end = 0 in_fmt = False diff --git a/pyupgrade/_plugins/set_literals.py b/pyupgrade/_plugins/set_literals.py index 631dc88a..0b3b21fa 100644 --- a/pyupgrade/_plugins/set_literals.py +++ b/pyupgrade/_plugins/set_literals.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import Token @@ -50,6 +50,7 @@ def visit_Call( parent: ast.AST, ) -> Iterable[tuple[Offset, TokenFunc]]: if ( + not isinstance(parent, ast.FormattedValue) and isinstance(node.func, ast.Name) and node.func.id == 'set' and len(node.args) == 1 and diff --git a/pyupgrade/_plugins/shlex_join.py b/pyupgrade/_plugins/shlex_join.py index 14067560..ddd5824a 100644 --- a/pyupgrade/_plugins/shlex_join.py +++ b/pyupgrade/_plugins/shlex_join.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import NON_CODING_TOKENS from tokenize_rt import Offset @@ -39,7 +39,7 @@ def visit_Call( if ( isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Constant) and - isinstance(node.func.value.value, str) and + node.func.value.value == ' ' and node.func.attr == 'join' and not node.keywords and len(node.args) == 1 and diff --git a/pyupgrade/_plugins/six_base_classes.py b/pyupgrade/_plugins/six_base_classes.py index 77008bce..789fca6f 100644 --- a/pyupgrade/_plugins/six_base_classes.py +++ b/pyupgrade/_plugins/six_base_classes.py @@ -1,7 +1,7 @@ from __future__ import annotations import ast -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset diff --git a/pyupgrade/_plugins/six_calls.py b/pyupgrade/_plugins/six_calls.py index 2434a66f..7bddce84 100644 --- a/pyupgrade/_plugins/six_calls.py +++ b/pyupgrade/_plugins/six_calls.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import Token diff --git a/pyupgrade/_plugins/six_metaclasses.py b/pyupgrade/_plugins/six_metaclasses.py index 9d9e6eb9..b2d47311 100644 --- a/pyupgrade/_plugins/six_metaclasses.py +++ b/pyupgrade/_plugins/six_metaclasses.py @@ -1,7 +1,7 @@ from __future__ import annotations import ast -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import NON_CODING_TOKENS from tokenize_rt import Offset diff --git a/pyupgrade/_plugins/six_remove_decorators.py b/pyupgrade/_plugins/six_remove_decorators.py index 6998ac70..8d7f6178 100644 --- a/pyupgrade/_plugins/six_remove_decorators.py +++ b/pyupgrade/_plugins/six_remove_decorators.py @@ -1,7 +1,7 @@ from __future__ import annotations import ast -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset diff --git a/pyupgrade/_plugins/six_simple.py b/pyupgrade/_plugins/six_simple.py index d490d9e9..cbdf6897 100644 --- a/pyupgrade/_plugins/six_simple.py +++ b/pyupgrade/_plugins/six_simple.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset diff --git a/pyupgrade/_plugins/subprocess_run.py b/pyupgrade/_plugins/subprocess_run.py index 0452e48d..a4534c7d 100644 --- a/pyupgrade/_plugins/subprocess_run.py +++ b/pyupgrade/_plugins/subprocess_run.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import Token diff --git a/pyupgrade/_plugins/type_of_primitive.py b/pyupgrade/_plugins/type_of_primitive.py index 3dd834b3..b32a3146 100644 --- a/pyupgrade/_plugins/type_of_primitive.py +++ b/pyupgrade/_plugins/type_of_primitive.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import Token diff --git a/pyupgrade/_plugins/typing_classes.py b/pyupgrade/_plugins/typing_classes.py index 26de3b1b..92b7c925 100644 --- a/pyupgrade/_plugins/typing_classes.py +++ b/pyupgrade/_plugins/typing_classes.py @@ -2,8 +2,7 @@ import ast import functools -import sys -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import Token @@ -24,19 +23,13 @@ def _unparse(node: ast.expr) -> str: elif isinstance(node, ast.Attribute): return ''.join((_unparse(node.value), '.', node.attr)) elif isinstance(node, ast.Subscript): - if sys.version_info >= (3, 9): # pragma: >=3.9 cover - node_slice: ast.expr = node.slice - elif isinstance(node.slice, ast.Index): # pragma: <3.9 cover - node_slice = node.slice.value - else: - raise AssertionError(f'expected Slice: {ast.dump(node)}') - if isinstance(node_slice, ast.Tuple): - if len(node_slice.elts) == 1: - slice_s = f'{_unparse(node_slice.elts[0])},' + if isinstance(node.slice, ast.Tuple): + if len(node.slice.elts) == 1: + slice_s = f'{_unparse(node.slice.elts[0])},' else: - slice_s = ', '.join(_unparse(elt) for elt in node_slice.elts) + slice_s = ', '.join(_unparse(elt) for elt in node.slice.elts) else: - slice_s = _unparse(node_slice) + slice_s = _unparse(node.slice) return f'{_unparse(node.value)}[{slice_s}]' elif ( isinstance(node, ast.Constant) and diff --git a/pyupgrade/_plugins/typing_pep563.py b/pyupgrade/_plugins/typing_pep563.py index 4bf40c39..a4fd368c 100644 --- a/pyupgrade/_plugins/typing_pep563.py +++ b/pyupgrade/_plugins/typing_pep563.py @@ -3,8 +3,8 @@ import ast import functools import sys -from typing import Iterable -from typing import Sequence +from collections.abc import Iterable +from collections.abc import Sequence from tokenize_rt import Offset @@ -90,15 +90,8 @@ def _process_call(node: ast.Call) -> Iterable[ast.AST]: def _process_subscript(node: ast.Subscript) -> Iterable[ast.AST]: name = _get_name(node.value) if name == 'Annotated': - if sys.version_info >= (3, 9): # pragma: >=3.9 cover - node_slice = node.slice - elif isinstance(node.slice, ast.Index): # pragma: <3.9 cover - node_slice: ast.AST = node.slice.value - else: # pragma: <3.9 cover - node_slice = node.slice - - if isinstance(node_slice, ast.Tuple) and node_slice.elts: - yield node_slice.elts[0] + if isinstance(node.slice, ast.Tuple) and node.slice.elts: + yield node.slice.elts[0] elif name != 'Literal': yield node.slice diff --git a/pyupgrade/_plugins/typing_pep585.py b/pyupgrade/_plugins/typing_pep585.py index e036c3f7..77dbd7e3 100644 --- a/pyupgrade/_plugins/typing_pep585.py +++ b/pyupgrade/_plugins/typing_pep585.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset diff --git a/pyupgrade/_plugins/typing_pep604.py b/pyupgrade/_plugins/typing_pep604.py index c7b53bc0..9b4039f2 100644 --- a/pyupgrade/_plugins/typing_pep604.py +++ b/pyupgrade/_plugins/typing_pep604.py @@ -3,7 +3,7 @@ import ast import functools import sys -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import NON_CODING_TOKENS from tokenize_rt import Offset @@ -153,14 +153,7 @@ def visit_Subscript( # don't rewrite forward annotations (unless we know they will be dequoted) if 'annotations' not in state.from_imports['__future__']: - if ( - (sys.version_info >= (3, 9) and _any_arg_is_str(node.slice)) or - ( - sys.version_info < (3, 9) and - isinstance(node.slice, ast.Index) and - _any_arg_is_str(node.slice.value) - ) - ): + if _any_arg_is_str(node.slice): return if is_name_attr( @@ -171,19 +164,12 @@ def visit_Subscript( ): yield ast_to_offset(node), _fix_optional elif is_name_attr(node.value, state.from_imports, ('typing',), ('Union',)): - if sys.version_info >= (3, 9): # pragma: >=3.9 cover - node_slice = node.slice - elif isinstance(node.slice, ast.Index): # pragma: <3.9 cover - node_slice: ast.AST = node.slice.value - else: # pragma: <3.9 cover - node_slice = node.slice # unexpected slice type - - if isinstance(node_slice, ast.Slice): # not a valid annotation + if isinstance(node.slice, ast.Slice): # not a valid annotation return - if isinstance(node_slice, ast.Tuple): - if node_slice.elts: - arg_count = len(node_slice.elts) + if isinstance(node.slice, ast.Tuple): + if node.slice.elts: + arg_count = len(node.slice.elts) else: return # empty Union else: diff --git a/pyupgrade/_plugins/typing_pep646_unpack.py b/pyupgrade/_plugins/typing_pep646_unpack.py index 632814a7..9569d3c2 100644 --- a/pyupgrade/_plugins/typing_pep646_unpack.py +++ b/pyupgrade/_plugins/typing_pep646_unpack.py @@ -1,7 +1,7 @@ from __future__ import annotations import ast -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import Token @@ -35,5 +35,44 @@ def visit_Subscript( return if is_name_attr(node.value, state.from_imports, ('typing',), ('Unpack',)): - if isinstance(parent, (ast.Subscript, ast.Index)): + if isinstance(parent, ast.Subscript): yield ast_to_offset(node.value), _replace_unpack_with_star + + +def _visit_func( + state: State, + node: ast.AsyncFunctionDef | ast.FunctionDef, + parent: ast.AST, +) -> Iterable[tuple[Offset, TokenFunc]]: + if state.settings.min_version < (3, 11): + return + + vararg = node.args.vararg + if ( + vararg is not None and + isinstance(vararg.annotation, ast.Subscript) and + is_name_attr( + vararg.annotation.value, + state.from_imports, + ('typing',), ('Unpack',), + ) + ): + yield ast_to_offset(vararg.annotation.value), _replace_unpack_with_star + + +@register(ast.AsyncFunctionDef) +def visit_AsyncFunctionDef( + state: State, + node: ast.AsyncFunctionDef, + parent: ast.AST, +) -> Iterable[tuple[Offset, TokenFunc]]: + yield from _visit_func(state, node, parent) + + +@register(ast.FunctionDef) +def visit_FunctionDef( + state: State, + node: ast.FunctionDef, + parent: ast.AST, +) -> Iterable[tuple[Offset, TokenFunc]]: + yield from _visit_func(state, node, parent) diff --git a/pyupgrade/_plugins/typing_pep696_typevar_defaults.py b/pyupgrade/_plugins/typing_pep696_typevar_defaults.py new file mode 100644 index 00000000..97f00f2e --- /dev/null +++ b/pyupgrade/_plugins/typing_pep696_typevar_defaults.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import ast +from collections.abc import Iterable + +from tokenize_rt import Offset +from tokenize_rt import Token + +from pyupgrade._ast_helpers import ast_to_offset +from pyupgrade._ast_helpers import is_name_attr +from pyupgrade._data import register +from pyupgrade._data import State +from pyupgrade._data import TokenFunc +from pyupgrade._token_helpers import find_op +from pyupgrade._token_helpers import parse_call_args + + +def _fix_typevar_default(i: int, tokens: list[Token]) -> None: + j = find_op(tokens, i, '[') + args, end = parse_call_args(tokens, j) + # remove the trailing `None` arguments + del tokens[args[0][1]:args[-1][1]] + + +def _should_rewrite(state: State) -> bool: + return ( + state.settings.min_version >= (3, 13) or ( + not state.settings.keep_runtime_typing and + state.in_annotation and + 'annotations' in state.from_imports['__future__'] + ) + ) + + +def _is_none(node: ast.AST) -> bool: + return isinstance(node, ast.Constant) and node.value is None + + +@register(ast.Subscript) +def visit_Subscript( + state: State, + node: ast.Subscript, + parent: ast.AST, +) -> Iterable[tuple[Offset, TokenFunc]]: + if not _should_rewrite(state): + return + + if ( + is_name_attr( + node.value, + state.from_imports, + ('collections.abc', 'typing', 'typing_extensions'), + ('Generator',), + ) and + isinstance(node.slice, ast.Tuple) and + len(node.slice.elts) == 3 and + _is_none(node.slice.elts[1]) and + _is_none(node.slice.elts[2]) + ): + yield ast_to_offset(node), _fix_typevar_default + elif ( + is_name_attr( + node.value, + state.from_imports, + ('collections.abc', 'typing', 'typing_extensions'), + ('AsyncGenerator',), + ) and + isinstance(node.slice, ast.Tuple) and + len(node.slice.elts) == 2 and + _is_none(node.slice.elts[1]) + ): + yield ast_to_offset(node), _fix_typevar_default diff --git a/pyupgrade/_plugins/typing_text.py b/pyupgrade/_plugins/typing_text.py index 031504b4..4e06dd36 100644 --- a/pyupgrade/_plugins/typing_text.py +++ b/pyupgrade/_plugins/typing_text.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset diff --git a/pyupgrade/_plugins/unittest_aliases.py b/pyupgrade/_plugins/unittest_aliases.py index 4b1a745d..9afe9552 100644 --- a/pyupgrade/_plugins/unittest_aliases.py +++ b/pyupgrade/_plugins/unittest_aliases.py @@ -2,7 +2,7 @@ import ast import functools -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset diff --git a/pyupgrade/_plugins/unpack_list_comprehension.py b/pyupgrade/_plugins/unpack_list_comprehension.py index ce072451..546a3dee 100644 --- a/pyupgrade/_plugins/unpack_list_comprehension.py +++ b/pyupgrade/_plugins/unpack_list_comprehension.py @@ -1,7 +1,7 @@ from __future__ import annotations import ast -from typing import Iterable +from collections.abc import Iterable from tokenize_rt import Offset from tokenize_rt import Token diff --git a/pyupgrade/_plugins/versioned_branches.py b/pyupgrade/_plugins/versioned_branches.py index 05c776ac..1b6d3614 100644 --- a/pyupgrade/_plugins/versioned_branches.py +++ b/pyupgrade/_plugins/versioned_branches.py @@ -1,8 +1,8 @@ from __future__ import annotations import ast +from collections.abc import Iterable from typing import cast -from typing import Iterable from tokenize_rt import Offset from tokenize_rt import Token diff --git a/pyupgrade/_string_helpers.py b/pyupgrade/_string_helpers.py index 1c15655d..7855e970 100644 --- a/pyupgrade/_string_helpers.py +++ b/pyupgrade/_string_helpers.py @@ -1,14 +1,13 @@ from __future__ import annotations import codecs -import re import string from typing import Optional -from typing import Tuple -NAMED_UNICODE_RE = re.compile(r'(? str: return ''.join(_convert_tup(tup) for tup in parsed) -def curly_escape(s: str) -> str: - parts = NAMED_UNICODE_RE.split(s) - return ''.join( - part.replace('{', '{{').replace('}', '}}') - if not NAMED_UNICODE_RE.fullmatch(part) - else part - for part in parts - ) - - def is_codec(encoding: str, name: str) -> bool: try: return codecs.lookup(encoding).name == name diff --git a/pyupgrade/_token_helpers.py b/pyupgrade/_token_helpers.py index 5936ab75..f55722d6 100644 --- a/pyupgrade/_token_helpers.py +++ b/pyupgrade/_token_helpers.py @@ -2,8 +2,8 @@ import ast import keyword +from collections.abc import Sequence from typing import NamedTuple -from typing import Sequence from tokenize_rt import NON_CODING_TOKENS from tokenize_rt import Token @@ -369,6 +369,14 @@ def arg_str(tokens: list[Token], start: int, end: int) -> str: return tokens_to_src(tokens[start:end]).strip() +def _arg_str_no_comment(tokens: list[Token], start: int, end: int) -> str: + arg_tokens = [ + token for token in tokens[start:end] + if token.name != 'COMMENT' + ] + return tokens_to_src(arg_tokens).strip() + + def _arg_contains_newline(tokens: list[Token], start: int, end: int) -> bool: while tokens[start].name in {'NL', 'NEWLINE', UNIMPORTANT_WS}: start += 1 @@ -473,7 +481,7 @@ def replace_argument( def constant_fold_tuple(i: int, tokens: list[Token]) -> None: start = find_op(tokens, i, '(') func_args, end = parse_call_args(tokens, start) - arg_strs = [arg_str(tokens, *arg) for arg in func_args] + arg_strs = [_arg_str_no_comment(tokens, *arg) for arg in func_args] unique_args = tuple(dict.fromkeys(arg_strs)) diff --git a/setup.cfg b/setup.cfg index 53fbdb2d..c496330b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 3.15.0 +version = 3.19.0 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown @@ -19,8 +19,8 @@ classifiers = [options] packages = find: install_requires = - tokenize-rt>=5.2.0 -python_requires = >=3.8.1 + tokenize-rt>=6.1.0 +python_requires = >=3.9 [options.packages.find] exclude = diff --git a/tests/features/dict_literals_test.py b/tests/features/dict_literals_test.py index e1b33bf1..880a01be 100644 --- a/tests/features/dict_literals_test.py +++ b/tests/features/dict_literals_test.py @@ -19,6 +19,10 @@ # Don't rewrite kwargd dicts 'dict(((a, b) for a, b in y), x=1)', 'dict(((a, b) for a, b in y), **kwargs)', + pytest.param( + 'f"{dict((a, b) for a, b in y)}"', + id='directly inside f-string placeholder', + ), ), ) def test_fix_dict_noop(s): diff --git a/tests/features/exceptions_test.py b/tests/features/exceptions_test.py index e855bf2a..335e1d4d 100644 --- a/tests/features/exceptions_test.py +++ b/tests/features/exceptions_test.py @@ -225,6 +225,30 @@ def test_fix_exceptions_version_specific_noop(s, version): id='leave unrelated error names alone', ), + pytest.param( + 'try: ...\n' + 'except (\n' + ' BaseException,\n' + ' BaseException # b\n' + '): ...\n', + + 'try: ...\n' + 'except BaseException: ...\n', + + id='dedupe with comment. see #932', + ), + pytest.param( + 'try: ...\n' + 'except (\n' + ' A, A,\n' + ' B # b\n' + '): ...\n', + + 'try: ...\n' + 'except (A, B): ...\n', + + id='dedupe other exception, one contains comment. see #932', + ), ), ) def test_fix_exceptions(s, expected): diff --git a/tests/features/set_literals_test.py b/tests/features/set_literals_test.py index 15251812..64827ba9 100644 --- a/tests/features/set_literals_test.py +++ b/tests/features/set_literals_test.py @@ -14,6 +14,14 @@ # Don't touch weird looking function calls -- use autopep8 or such # first 'set ((1, 2))', + pytest.param( + 'f"{set((1, 2))}"', + id='set directly inside f-string placeholder', + ), + pytest.param( + 'f"{set(x for x in y)}"', + id='set comp directly inside f-string placeholder', + ), ), ) def test_fix_sets_noop(s): diff --git a/tests/features/shlex_join_test.py b/tests/features/shlex_join_test.py index fc6abe59..e3835f65 100644 --- a/tests/features/shlex_join_test.py +++ b/tests/features/shlex_join_test.py @@ -15,6 +15,12 @@ (3, 8), id='quote from-imported', ), + pytest.param( + 'import shlex\n' + '"wat".join(shlex.quote(arg) for arg in cmd)\n', + (3, 8), + id='not joined with space', + ), pytest.param( 'import shlex\n' '" ".join(shlex.quote(arg) for arg in cmd)\n', diff --git a/tests/features/typing_pep646_unpack_test.py b/tests/features/typing_pep646_unpack_test.py index c1d82e85..d63909ad 100644 --- a/tests/features/typing_pep646_unpack_test.py +++ b/tests/features/typing_pep646_unpack_test.py @@ -21,6 +21,14 @@ ' pass', id='Not inside a subscript', ), + pytest.param( + 'from typing import Unpack\n' + 'from typing import TypedDict\n' + 'class D(TypedDict):\n' + ' x: int\n' + 'def f(**kwargs: Unpack[D]) -> None: pass\n', + id='3.12 TypedDict for kwargs', + ), ), ) def test_fix_pep646_noop(s): @@ -53,6 +61,15 @@ def test_fix_pep646_noop(s): 'class C(Generic[*Shape]):\n' ' pass', ), + pytest.param( + 'from typing import Unpack\n' + 'def f(*args: Unpack[tuple[int, ...]]): pass\n', + + 'from typing import Unpack\n' + 'def f(*args: *tuple[int, ...]): pass\n', + + id='Unpack for *args', + ), ), ) def test_typing_unpack(s, expected): diff --git a/tests/features/typing_pep696_typevar_defaults_test.py b/tests/features/typing_pep696_typevar_defaults_test.py new file mode 100644 index 00000000..99bb5f4a --- /dev/null +++ b/tests/features/typing_pep696_typevar_defaults_test.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import pytest + +from pyupgrade._data import Settings +from pyupgrade._main import _fix_plugins + + +@pytest.mark.parametrize( + ('s', 'version'), + ( + pytest.param( + 'from collections.abc import Generator\n' + 'def f() -> Generator[int, None, None]: yield 1\n', + (3, 12), + id='not 3.13+, no __future__.annotations', + ), + pytest.param( + 'from __future__ import annotations\n' + 'from collections.abc import Generator\n' + 'def f() -> Generator[int]: yield 1\n', + (3, 12), + id='already converted!', + ), + pytest.param( + 'from __future__ import annotations\n' + 'from collections.abc import Generator\n' + 'def f() -> Generator[int, int, None]: yield 1\n' + 'def g() -> Generator[int, int, int]: yield 1\n', + (3, 12), + id='non-None send/return type', + ), + ), +) +def test_fix_pep696_noop(s, version): + assert _fix_plugins(s, settings=Settings(min_version=version)) == s + + +def test_fix_pep696_noop_keep_runtime_typing(): + settings = Settings(min_version=(3, 12), keep_runtime_typing=True) + s = '''\ +from __future__ import annotations +from collections.abc import Generator +def f() -> Generator[int, None, None]: yield 1 +''' + assert _fix_plugins(s, settings=settings) == s + + +@pytest.mark.parametrize( + ('s', 'expected'), + ( + pytest.param( + 'from __future__ import annotations\n' + 'from typing import Generator\n' + 'def f() -> Generator[int, None, None]: yield 1\n', + + 'from __future__ import annotations\n' + 'from collections.abc import Generator\n' + 'def f() -> Generator[int]: yield 1\n', + + id='typing.Generator', + ), + pytest.param( + 'from __future__ import annotations\n' + 'from typing_extensions import Generator\n' + 'def f() -> Generator[int, None, None]: yield 1\n', + + 'from __future__ import annotations\n' + 'from typing_extensions import Generator\n' + 'def f() -> Generator[int]: yield 1\n', + + id='typing_extensions.Generator', + ), + pytest.param( + 'from __future__ import annotations\n' + 'from collections.abc import Generator\n' + 'def f() -> Generator[int, None, None]: yield 1\n', + + 'from __future__ import annotations\n' + 'from collections.abc import Generator\n' + 'def f() -> Generator[int]: yield 1\n', + + id='collections.abc.Generator', + ), + pytest.param( + 'from __future__ import annotations\n' + 'from collections.abc import AsyncGenerator\n' + 'async def f() -> AsyncGenerator[int, None]: yield 1\n', + + 'from __future__ import annotations\n' + 'from collections.abc import AsyncGenerator\n' + 'async def f() -> AsyncGenerator[int]: yield 1\n', + + id='collections.abc.AsyncGenerator', + ), + ), +) +def test_fix_pep696_with_future_annotations(s, expected): + assert _fix_plugins(s, settings=Settings(min_version=(3, 12))) == expected + + +@pytest.mark.parametrize( + ('s', 'expected'), + ( + pytest.param( + 'from collections.abc import Generator\n' + 'def f() -> Generator[int, None, None]: yield 1\n', + + 'from collections.abc import Generator\n' + 'def f() -> Generator[int]: yield 1\n', + + id='Generator', + ), + pytest.param( + 'from collections.abc import AsyncGenerator\n' + 'async def f() -> AsyncGenerator[int, None]: yield 1\n', + + 'from collections.abc import AsyncGenerator\n' + 'async def f() -> AsyncGenerator[int]: yield 1\n', + + id='AsyncGenerator', + ), + ), +) +def test_fix_pep696_with_3_13(s, expected): + assert _fix_plugins(s, settings=Settings(min_version=(3, 13))) == expected diff --git a/tests/main_test.py b/tests/main_test.py index a254573a..bf595db3 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -25,6 +25,8 @@ def test_main_noop(tmpdir, capsys): x=version_info def f(): global x, y + +f'hello snowman: \\N{SNOWMAN}' ''' f = tmpdir.join('f.py') f.write(s)